From 26b3cac486610c3453647cf33ebe1bf647e1ef08 Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Fri, 9 Aug 2024 13:36:32 +1000 Subject: [PATCH 01/14] feat(backend): ratings CRUD draft --- .../20240406031915_create_applications.sql | 2 +- backend/server/src/handler/mod.rs | 1 + backend/server/src/handler/ratings.rs | 61 +++++++++ backend/server/src/main.rs | 11 ++ backend/server/src/models/mod.rs | 1 + backend/server/src/models/ratings.rs | 122 ++++++++++++++++++ 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 backend/server/src/handler/ratings.rs create mode 100644 backend/server/src/models/ratings.rs diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index 767abb92..f80f3a6d 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -91,7 +91,7 @@ CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_an CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); CREATE TABLE application_ratings ( - id SERIAL PRIMARY KEY, + id BIGINT PRIMARY KEY, application_id BIGINT NOT NULL, rater_id BIGINT NOT NULL, rating INTEGER NOT NULL, diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 163c4759..b30ae41f 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod organisation; +pub mod ratings; diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs new file mode 100644 index 00000000..edbbc39b --- /dev/null +++ b/backend/server/src/handler/ratings.rs @@ -0,0 +1,61 @@ +use crate::models::app::AppState; +use crate::models::auth::SuperUser; +use crate::models::error::ChaosError; +use crate::models::ratings::{NewRating, Rating}; +use crate::models::transaction::DBTransaction; +use axum::extract::{Json, Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; + +pub struct RatingsHandler; + +impl RatingsHandler { + // TODO: are all the user permissions as required? Who should be able to do what with ratings? + pub async fn create_rating( + State(state): State, + _user: SuperUser, + mut transaction: DBTransaction<'_>, + Json(new_rating): Json, + ) -> Result { + Rating::create(new_rating, state.snowflake_generator, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully created rating")) + } + + pub async fn get_ratings_for_application( + State(_state): State, + Path(application_id): Path, + _user: SuperUser, + mut transaction: DBTransaction<'_>, + ) -> Result { + let ratings = + Rating::get_all_ratings_from_application_id(application_id, &mut transaction.tx) + .await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, Json(ratings))) + } + + // TODO: should I use transaction here? Organisation still uses state.db for these simpler getters. + pub async fn get( + State(_state): State, + Path(rating_id): Path, + _user: SuperUser, + mut transaction: DBTransaction<'_>, + ) -> Result { + let org = Rating::get_rating(rating_id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, Json(org))) + } + + // TODO: should I use transaction here? + pub async fn delete( + State(_state): State, + Path(id): Path, + _user: SuperUser, + mut transaction: DBTransaction<'_>, + ) -> Result { + Rating::delete(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully deleted rating")) + } +} diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 38b2022c..d46bba4f 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -4,6 +4,7 @@ use crate::models::storage::Storage; use anyhow::Result; use axum::routing::{get, patch, post, put}; use axum::Router; +use handler::ratings::RatingsHandler; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; @@ -89,6 +90,16 @@ async fn main() -> Result<()> { .put(OrganisationHandler::update_admins) .delete(OrganisationHandler::remove_admin), ) + .route("/api/v1/ratings/:rating_id", get(RatingsHandler::get)) + .route( + "/api/v1/:application_id/rating", + put(RatingsHandler::create_rating), + ) + .route( + "/api/v1/:application_id/ratings", + get(RatingsHandler::get_ratings_for_application), + ) + // TODO: should ratings also have a delete endpoint? I think old backend didn't .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index e5a5edb3..f7dfc42b 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -3,6 +3,7 @@ pub mod auth; pub mod campaign; pub mod error; pub mod organisation; +pub mod ratings; pub mod storage; pub mod transaction; pub mod user; diff --git a/backend/server/src/models/ratings.rs b/backend/server/src/models/ratings.rs new file mode 100644 index 00000000..ed4de95f --- /dev/null +++ b/backend/server/src/models/ratings.rs @@ -0,0 +1,122 @@ +use crate::models::error::ChaosError; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use snowflake::SnowflakeIdGenerator; +use sqlx::{FromRow, Postgres, Transaction}; +use std::ops::DerefMut; + +#[derive(Deserialize, Serialize, Clone, FromRow, Debug)] +pub struct Rating { + pub id: i64, + pub application_id: i64, + pub rater_user_id: i64, + pub rating: i32, + // TODO: what's the point of storing created_at and updated_at if they are not accessible to users through endpoints? + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// TODO: Does the user have to provide rater user id in their JSON? How do they get that? can't we get that in the RatingsHandler::create_rating method? +#[derive(Deserialize, Serialize)] +pub struct NewRating { + pub application_id: i64, + pub rater_user_id: i64, + pub rating: i32, +} + +#[derive(Deserialize, Serialize)] +pub struct RatingDetails { + pub id: i64, + pub rating: i32, + pub created_at: DateTime, +} + +#[derive(Deserialize, Serialize)] +pub struct RatingsDetails { + // TODO: should this be: Vec instead? + pub ratings: Vec, +} + +impl Rating { + /// Create a new rating. + pub async fn create( + new_rating: NewRating, + mut snowflake_generator: SnowflakeIdGenerator, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + let rating_id = snowflake_generator.generate(); + let application_id = new_rating.application_id; + let rater_user_id = new_rating.rater_user_id; + let rating = new_rating.rating; + + sqlx::query!( + " + INSERT INTO application_ratings (id, application_id, rater_id, rating) + VALUES ($1, $2, $3, $4) + ", + rating_id, + application_id, + rater_user_id, + rating + ) + .execute(transaction.deref_mut()) + .await?; + + Ok(()) + } + + pub async fn get_rating( + rating_id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { + let rating = sqlx::query_as!( + RatingDetails, + " + SELECT id, rating, created_at + FROM application_ratings + WHERE id = $1 + ", + rating_id + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(rating) + } + + /// Get all ratings for a certain application. + pub async fn get_all_ratings_from_application_id( + application_id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { + let ratings = sqlx::query_as!( + RatingDetails, + " + SELECT id, rating, created_at + FROM application_ratings + WHERE application_id = $1 + ", + application_id + ) + .fetch_all(transaction.deref_mut()) + .await?; + + Ok(RatingsDetails { ratings }) + } + + pub async fn delete( + rating_id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + sqlx::query!( + " + DELETE FROM application_ratings WHERE id = $1 + ", + rating_id + ) + .execute(transaction.deref_mut()) + .await?; + + Ok(()) + } +} From 09e41d1fe740e9bd43d26fa51271947dd5d3ec0f Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Mon, 30 Sep 2024 16:57:49 +1000 Subject: [PATCH 02/14] backend: added auth to ratings CRUD --- backend/server/src/handler/ratings.rs | 12 +++--- backend/server/src/main.rs | 8 ++-- backend/server/src/models/auth.rs | 47 +++++++++++++++++++++- backend/server/src/models/ratings.rs | 1 + backend/server/src/service/mod.rs | 1 + backend/server/src/service/organisation.rs | 2 +- backend/server/src/service/ratings.rs | 47 ++++++++++++++++++++++ 7 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 backend/server/src/service/ratings.rs diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index edbbc39b..e5e565e3 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -1,5 +1,5 @@ use crate::models::app::AppState; -use crate::models::auth::SuperUser; +use crate::models::auth::{ApplicationReviewerAdmin, SuperUser}; use crate::models::error::ChaosError; use crate::models::ratings::{NewRating, Rating}; use crate::models::transaction::DBTransaction; @@ -13,7 +13,7 @@ impl RatingsHandler { // TODO: are all the user permissions as required? Who should be able to do what with ratings? pub async fn create_rating( State(state): State, - _user: SuperUser, + _admin: ApplicationReviewerAdmin, mut transaction: DBTransaction<'_>, Json(new_rating): Json, ) -> Result { @@ -25,7 +25,7 @@ impl RatingsHandler { pub async fn get_ratings_for_application( State(_state): State, Path(application_id): Path, - _user: SuperUser, + _admin: ApplicationReviewerAdmin, mut transaction: DBTransaction<'_>, ) -> Result { let ratings = @@ -35,11 +35,10 @@ impl RatingsHandler { Ok((StatusCode::OK, Json(ratings))) } - // TODO: should I use transaction here? Organisation still uses state.db for these simpler getters. pub async fn get( State(_state): State, Path(rating_id): Path, - _user: SuperUser, + _admin: ApplicationReviewerAdmin, mut transaction: DBTransaction<'_>, ) -> Result { let org = Rating::get_rating(rating_id, &mut transaction.tx).await?; @@ -47,11 +46,10 @@ impl RatingsHandler { Ok((StatusCode::OK, Json(org))) } - // TODO: should I use transaction here? pub async fn delete( State(_state): State, Path(id): Path, - _user: SuperUser, + _admin: ApplicationReviewerAdmin, mut transaction: DBTransaction<'_>, ) -> Result { Rating::delete(id, &mut transaction.tx).await?; diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index d46bba4f..4071d47b 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -2,7 +2,7 @@ use crate::handler::auth::google_callback; use crate::handler::organisation::OrganisationHandler; use crate::models::storage::Storage; use anyhow::Result; -use axum::routing::{get, patch, post, put}; +use axum::routing::{delete, get, patch, post, put}; use axum::Router; use handler::ratings::RatingsHandler; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; @@ -90,7 +90,10 @@ async fn main() -> Result<()> { .put(OrganisationHandler::update_admins) .delete(OrganisationHandler::remove_admin), ) - .route("/api/v1/ratings/:rating_id", get(RatingsHandler::get)) + .route( + "/api/v1/ratings/:rating_id", + get(RatingsHandler::get).delete(RatingsHandler::delete), + ) .route( "/api/v1/:application_id/rating", put(RatingsHandler::create_rating), @@ -99,7 +102,6 @@ async fn main() -> Result<()> { "/api/v1/:application_id/ratings", get(RatingsHandler::get_ratings_for_application), ) - // TODO: should ratings also have a delete endpoint? I think old backend didn't .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index b5e9c298..3b37a05e 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -2,7 +2,8 @@ use crate::models::app::AppState; use crate::models::error::ChaosError; use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; -use crate::service::organisation::user_is_admin; +use crate::service::organisation::assert_user_is_admin; +use crate::service::ratings::assert_user_is_application_reviewer_admin; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; @@ -139,8 +140,50 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - user_is_admin(user_id, organisation_id, pool).await?; + assert_user_is_admin(user_id, organisation_id, pool).await?; Ok(OrganisationAdmin { user_id }) } } + +pub struct ApplicationReviewerAdmin { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for ApplicationReviewerAdmin +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. + let app_state = AppState::from_ref(state); + let decoding_key = &app_state.decoding_key; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| ChaosError::NotLoggedIn)?; + + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + + let pool = &app_state.db; + let user_id = claims.sub; + + // TODO: How am I guaranteeing this rating id is in the endpoint? Would it be a bad request if the rating id was left out? + let Path(rating_id) = parts + .extract::>() + .await + .map_err(|_| ChaosError::BadRequest)?; + + assert_user_is_application_reviewer_admin(user_id, rating_id, pool).await?; + + Ok(ApplicationReviewerAdmin { user_id }) + } +} diff --git a/backend/server/src/models/ratings.rs b/backend/server/src/models/ratings.rs index ed4de95f..cbd7ec5e 100644 --- a/backend/server/src/models/ratings.rs +++ b/backend/server/src/models/ratings.rs @@ -108,6 +108,7 @@ impl Rating { rating_id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { + // TODO: fix sth kavika wanted fixed. sqlx::query!( " DELETE FROM application_ratings WHERE id = $1 diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index 5e708b6b..a19f470a 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -2,3 +2,4 @@ pub mod auth; pub mod jwt; pub mod oauth2; pub mod organisation; +pub mod ratings; diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index a29a1716..ac9c2b66 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -7,7 +7,7 @@ use sqlx::{Pool, Postgres, Transaction}; use std::ops::DerefMut; use uuid::Uuid; -pub async fn user_is_admin( +pub async fn assert_user_is_admin( user_id: i64, organisation_id: i64, pool: &Pool, diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs new file mode 100644 index 00000000..15353af9 --- /dev/null +++ b/backend/server/src/service/ratings.rs @@ -0,0 +1,47 @@ +use crate::models::campaign::Campaign; +use crate::models::error::ChaosError; +use crate::models::organisation::{Member, MemberList, OrganisationDetails, OrganisationRole}; +use chrono::{DateTime, Utc}; +use snowflake::SnowflakeIdGenerator; +use sqlx::{Pool, Postgres, Transaction}; +use std::ops::DerefMut; +use uuid::Uuid; + +pub async fn assert_user_is_application_reviewer_admin( + user_id: i64, + rating_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + // Any member of the organisation that owns the campaign is an application + // viewer, because all members are either directors or execs (TODO: might be + // changed in the future). + let is_admin = sqlx::query!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM organisation_members om + JOIN campaigns c ON om.organisation_id = c.organisation_id + JOIN applications a ON a.campaign_id = c.id + JOIN application_ratings ar ON ar.application_id = a.id + -- Find the organisation that this application rating belongs to + -- (via the campaign the rating belongs to). + WHERE ar.id = $1 + -- Assert user is member of the organisation that owns the campaign + -- this application belongs to. + AND om.user_id = $2 + ); + "#, + rating_id, + user_id + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if !is_admin { + return Err(ChaosError::Unauthorized); + } + + Ok(()) +} From 10f526f29aa149e7e3aa4bd1a07b708eb195c0c1 Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Mon, 14 Oct 2024 17:03:10 +1100 Subject: [PATCH 03/14] implemented most feedback --- backend/server/src/handler/ratings.rs | 22 +++++++++- backend/server/src/main.rs | 6 ++- backend/server/src/models/auth.rs | 47 +++++++++++++++++++- backend/server/src/models/ratings.rs | 63 +++++++++++++++++++-------- backend/server/src/service/ratings.rs | 16 +++++-- 5 files changed, 128 insertions(+), 26 deletions(-) diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index e5e565e3..43e848d0 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -13,15 +13,35 @@ impl RatingsHandler { // TODO: are all the user permissions as required? Who should be able to do what with ratings? pub async fn create_rating( State(state): State, + Path(application_id): Path, _admin: ApplicationReviewerAdmin, mut transaction: DBTransaction<'_>, Json(new_rating): Json, ) -> Result { - Rating::create(new_rating, state.snowflake_generator, &mut transaction.tx).await?; + Rating::create( + new_rating, + application_id, + state.snowflake_generator, + &mut transaction.tx, + ) + .await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully created rating")) } + pub async fn update( + State(_state): State, + Path(rating_id): Path, + // TODO: authorization: needs to be user that created the rating and is a current member of organisation. + _admin: ApplicationReviewerAdmin, + mut transaction: DBTransaction<'_>, + Json(updated_rating): Json, + ) -> Result { + Rating::update(rating_id, updated_rating, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully updated rating")) + } + pub async fn get_ratings_for_application( State(_state): State, Path(application_id): Path, diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 4071d47b..e96a06d6 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -92,11 +92,13 @@ async fn main() -> Result<()> { ) .route( "/api/v1/ratings/:rating_id", - get(RatingsHandler::get).delete(RatingsHandler::delete), + get(RatingsHandler::get) + .delete(RatingsHandler::delete) + .put(RatingsHandler::update), ) .route( "/api/v1/:application_id/rating", - put(RatingsHandler::create_rating), + post(RatingsHandler::create_rating), ) .route( "/api/v1/:application_id/ratings", diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 3b37a05e..e934e7d5 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -3,7 +3,9 @@ use crate::models::error::ChaosError; use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_admin; -use crate::service::ratings::assert_user_is_application_reviewer_admin; +use crate::service::ratings::{ + assert_user_is_application_reviewer_admin, assert_user_is_rating_creator, +}; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; @@ -176,6 +178,7 @@ where let pool = &app_state.db; let user_id = claims.sub; + // TODO: check if application_id or rating_id was passed. // TODO: How am I guaranteeing this rating id is in the endpoint? Would it be a bad request if the rating id was left out? let Path(rating_id) = parts .extract::>() @@ -187,3 +190,45 @@ where Ok(ApplicationReviewerAdmin { user_id }) } } + +pub struct RatingCreatorAdmin { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for RatingCreatorAdmin +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. + let app_state = AppState::from_ref(state); + let decoding_key = &app_state.decoding_key; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| ChaosError::NotLoggedIn)?; + + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + + let pool = &app_state.db; + let user_id = claims.sub; + + // TODO + // let Path(rating_id) = parts + // .extract::>() + // .await + // .map_err(|_| ChaosError::BadRequest)?; + + // assert_user_is_rating_creator(user_id, rating_id, pool).await?; + + Ok(RatingCreatorAdmin { user_id }) + } +} diff --git a/backend/server/src/models/ratings.rs b/backend/server/src/models/ratings.rs index cbd7ec5e..dcf1ab24 100644 --- a/backend/server/src/models/ratings.rs +++ b/backend/server/src/models/ratings.rs @@ -11,15 +11,12 @@ pub struct Rating { pub application_id: i64, pub rater_user_id: i64, pub rating: i32, - // TODO: what's the point of storing created_at and updated_at if they are not accessible to users through endpoints? pub created_at: DateTime, pub updated_at: DateTime, } -// TODO: Does the user have to provide rater user id in their JSON? How do they get that? can't we get that in the RatingsHandler::create_rating method? #[derive(Deserialize, Serialize)] pub struct NewRating { - pub application_id: i64, pub rater_user_id: i64, pub rating: i32, } @@ -27,13 +24,14 @@ pub struct NewRating { #[derive(Deserialize, Serialize)] pub struct RatingDetails { pub id: i64, + pub rater_id: i64, + pub rater_name: String, pub rating: i32, - pub created_at: DateTime, + pub updated_at: DateTime, } #[derive(Deserialize, Serialize)] -pub struct RatingsDetails { - // TODO: should this be: Vec instead? +pub struct ApplicationRatings { pub ratings: Vec, } @@ -41,11 +39,11 @@ impl Rating { /// Create a new rating. pub async fn create( new_rating: NewRating, + application_id: i64, mut snowflake_generator: SnowflakeIdGenerator, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { let rating_id = snowflake_generator.generate(); - let application_id = new_rating.application_id; let rater_user_id = new_rating.rater_user_id; let rating = new_rating.rating; @@ -65,6 +63,32 @@ impl Rating { Ok(()) } + /// Create a new rating. + pub async fn update( + rating_id: i64, + updated_rating: NewRating, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + let rating = updated_rating.rating; + let current_time = Utc::now(); + + let _ = sqlx::query!( + " + UPDATE application_ratings + SET rating = $2, updated_at = $3 + WHERE id = $1 + RETURNING id; + ", + rating_id, + rating, + current_time + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(()) + } + pub async fn get_rating( rating_id: i64, transaction: &mut Transaction<'_, Postgres>, @@ -72,9 +96,10 @@ impl Rating { let rating = sqlx::query_as!( RatingDetails, " - SELECT id, rating, created_at - FROM application_ratings - WHERE id = $1 + SELECT r.id, rater_id, u.name as rater_name, r.rating, r.updated_at + FROM application_ratings r + JOIN users u ON u.id = r.id + WHERE r.id = $1 ", rating_id ) @@ -88,34 +113,36 @@ impl Rating { pub async fn get_all_ratings_from_application_id( application_id: i64, transaction: &mut Transaction<'_, Postgres>, - ) -> Result { + ) -> Result { let ratings = sqlx::query_as!( RatingDetails, " - SELECT id, rating, created_at - FROM application_ratings - WHERE application_id = $1 + SELECT r.id, rater_id, u.name as rater_name, r.rating, r.updated_at + FROM application_ratings r + JOIN users u ON u.id = r.id + WHERE r.application_id = $1 ", application_id ) .fetch_all(transaction.deref_mut()) .await?; - Ok(RatingsDetails { ratings }) + Ok(ApplicationRatings { ratings }) } pub async fn delete( rating_id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // TODO: fix sth kavika wanted fixed. - sqlx::query!( + // Throws error if rating id doesn't exist. + let _ = sqlx::query!( " DELETE FROM application_ratings WHERE id = $1 + RETURNING id; ", rating_id ) - .execute(transaction.deref_mut()) + .fetch_one(transaction.deref_mut()) .await?; Ok(()) diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs index 15353af9..7a90fb24 100644 --- a/backend/server/src/service/ratings.rs +++ b/backend/server/src/service/ratings.rs @@ -7,14 +7,14 @@ use sqlx::{Pool, Postgres, Transaction}; use std::ops::DerefMut; use uuid::Uuid; +/// Any member of the organisation that owns the campaign is an application +/// viewer, because all members are either directors or execs (TODO: might be +/// changed in the future). pub async fn assert_user_is_application_reviewer_admin( user_id: i64, rating_id: i64, pool: &Pool, ) -> Result<(), ChaosError> { - // Any member of the organisation that owns the campaign is an application - // viewer, because all members are either directors or execs (TODO: might be - // changed in the future). let is_admin = sqlx::query!( r#" SELECT EXISTS ( @@ -29,7 +29,7 @@ pub async fn assert_user_is_application_reviewer_admin( -- Assert user is member of the organisation that owns the campaign -- this application belongs to. AND om.user_id = $2 - ); + ) "#, rating_id, user_id @@ -45,3 +45,11 @@ pub async fn assert_user_is_application_reviewer_admin( Ok(()) } + +pub async fn assert_user_is_rating_creator( + user_id: i64, + rating_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + todo!() +} From de66c175820c554ef49fdb2a1d36101dc75650de Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Sun, 20 Oct 2024 23:47:18 +1100 Subject: [PATCH 04/14] fixed ratings authorisation --- backend/server/src/handler/ratings.rs | 21 ++++---- backend/server/src/models/auth.rs | 77 +++++++++++++++++++++----- backend/server/src/service/ratings.rs | 78 +++++++++++++++++++++++++-- 3 files changed, 151 insertions(+), 25 deletions(-) diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index 43e848d0..05d4658b 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -1,5 +1,8 @@ use crate::models::app::AppState; -use crate::models::auth::{ApplicationReviewerAdmin, SuperUser}; +use crate::models::auth::{ + ApplicationReviewerAdminGivenApplicationId, ApplicationReviewerAdminGivenRatingId, + RatingCreatorAdmin, SuperUser, +}; use crate::models::error::ChaosError; use crate::models::ratings::{NewRating, Rating}; use crate::models::transaction::DBTransaction; @@ -14,7 +17,8 @@ impl RatingsHandler { pub async fn create_rating( State(state): State, Path(application_id): Path, - _admin: ApplicationReviewerAdmin, + // TODO: Potential bug: the check whether user is allowed to access rating doesn't make sense here, because no rating exists yet + _admin: ApplicationReviewerAdminGivenApplicationId, mut transaction: DBTransaction<'_>, Json(new_rating): Json, ) -> Result { @@ -32,8 +36,7 @@ impl RatingsHandler { pub async fn update( State(_state): State, Path(rating_id): Path, - // TODO: authorization: needs to be user that created the rating and is a current member of organisation. - _admin: ApplicationReviewerAdmin, + _admin: RatingCreatorAdmin, mut transaction: DBTransaction<'_>, Json(updated_rating): Json, ) -> Result { @@ -45,7 +48,7 @@ impl RatingsHandler { pub async fn get_ratings_for_application( State(_state): State, Path(application_id): Path, - _admin: ApplicationReviewerAdmin, + _admin: ApplicationReviewerAdminGivenApplicationId, mut transaction: DBTransaction<'_>, ) -> Result { let ratings = @@ -58,7 +61,7 @@ impl RatingsHandler { pub async fn get( State(_state): State, Path(rating_id): Path, - _admin: ApplicationReviewerAdmin, + _admin: ApplicationReviewerAdminGivenRatingId, mut transaction: DBTransaction<'_>, ) -> Result { let org = Rating::get_rating(rating_id, &mut transaction.tx).await?; @@ -68,11 +71,11 @@ impl RatingsHandler { pub async fn delete( State(_state): State, - Path(id): Path, - _admin: ApplicationReviewerAdmin, + Path(rating_id): Path, + _admin: ApplicationReviewerAdminGivenRatingId, mut transaction: DBTransaction<'_>, ) -> Result { - Rating::delete(id, &mut transaction.tx).await?; + Rating::delete(rating_id, &mut transaction.tx).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully deleted rating")) } diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index e934e7d5..a462bc7b 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -4,7 +4,9 @@ use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_admin; use crate::service::ratings::{ - assert_user_is_application_reviewer_admin, assert_user_is_rating_creator, + assert_user_is_application_reviewer_admin_given_application_id, + assert_user_is_application_reviewer_admin_given_rating_id, + assert_user_is_rating_creator_and_organisation_member, }; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; @@ -148,12 +150,64 @@ where } } -pub struct ApplicationReviewerAdmin { +// TODO: Not very idiomatic way. The reason this impl was chosen was because we +// couldn't figure out how to dynamically check whether the id passed in path +// was a rating id or an application id. + +/// Get the application reviewer given a path that contains the application id. +pub struct ApplicationReviewerAdminGivenApplicationId { pub user_id: i64, } #[async_trait] -impl FromRequestParts for ApplicationReviewerAdmin +impl FromRequestParts for ApplicationReviewerAdminGivenApplicationId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. + let app_state = AppState::from_ref(state); + let decoding_key = &app_state.decoding_key; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| ChaosError::NotLoggedIn)?; + + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + + let pool = &app_state.db; + let user_id = claims.sub; + + let Path(application_id) = parts + .extract::>() + .await + .map_err(|_| ChaosError::BadRequest)?; + + assert_user_is_application_reviewer_admin_given_application_id( + user_id, + application_id, + pool, + ) + .await?; + + Ok(ApplicationReviewerAdminGivenApplicationId { user_id }) + } +} + +/// Get the application reviewer given a path that contains the rating id. +pub struct ApplicationReviewerAdminGivenRatingId { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for ApplicationReviewerAdminGivenRatingId where AppState: FromRef, S: Send + Sync, @@ -178,16 +232,14 @@ where let pool = &app_state.db; let user_id = claims.sub; - // TODO: check if application_id or rating_id was passed. - // TODO: How am I guaranteeing this rating id is in the endpoint? Would it be a bad request if the rating id was left out? let Path(rating_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_application_reviewer_admin(user_id, rating_id, pool).await?; + assert_user_is_application_reviewer_admin_given_rating_id(user_id, rating_id, pool).await?; - Ok(ApplicationReviewerAdmin { user_id }) + Ok(ApplicationReviewerAdminGivenRatingId { user_id }) } } @@ -221,13 +273,12 @@ where let pool = &app_state.db; let user_id = claims.sub; - // TODO - // let Path(rating_id) = parts - // .extract::>() - // .await - // .map_err(|_| ChaosError::BadRequest)?; + let Path(rating_id) = parts + .extract::>() + .await + .map_err(|_| ChaosError::BadRequest)?; - // assert_user_is_rating_creator(user_id, rating_id, pool).await?; + assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, pool).await?; Ok(RatingCreatorAdmin { user_id }) } diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs index 7a90fb24..1d011ae1 100644 --- a/backend/server/src/service/ratings.rs +++ b/backend/server/src/service/ratings.rs @@ -10,7 +10,46 @@ use uuid::Uuid; /// Any member of the organisation that owns the campaign is an application /// viewer, because all members are either directors or execs (TODO: might be /// changed in the future). -pub async fn assert_user_is_application_reviewer_admin( +pub async fn assert_user_is_application_reviewer_admin_given_application_id( + user_id: i64, + application_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let is_admin = sqlx::query!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM organisation_members om + JOIN campaigns c ON om.organisation_id = c.organisation_id + JOIN applications a ON a.campaign_id = c.id + JOIN application_ratings ar ON a.campaign_id = c.id + -- Find the organisation that this application rating belongs to + -- (via the campaign the rating belongs to). + WHERE a.id = $1 + -- Assert user is member of the organisation that owns the campaign + -- this application belongs to. + AND om.user_id = $2 + ) + "#, + application_id, + user_id + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if !is_admin { + return Err(ChaosError::Unauthorized); + } + + Ok(()) +} + +/// Any member of the organisation that owns the campaign is an application +/// viewer, because all members are either directors or execs (TODO: might be +/// changed in the future). +pub async fn assert_user_is_application_reviewer_admin_given_rating_id( user_id: i64, rating_id: i64, pool: &Pool, @@ -46,10 +85,43 @@ pub async fn assert_user_is_application_reviewer_admin( Ok(()) } -pub async fn assert_user_is_rating_creator( +/// Assert the given user is the creator of the rating with the given id and +/// also a current member of the organisation the rating/application is +/// associated with. +pub async fn assert_user_is_rating_creator_and_organisation_member( user_id: i64, rating_id: i64, pool: &Pool, ) -> Result<(), ChaosError> { - todo!() + let is_admin = sqlx::query!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM organisation_members om + JOIN campaigns c ON om.organisation_id = c.organisation_id + JOIN applications a ON a.campaign_id = c.id + JOIN application_ratings ar ON ar.application_id = a.id + -- Find the organisation that this application rating belongs to + -- (via the campaign the rating belongs to). + WHERE ar.id = $1 + -- Assert user is the rater of the application rating. + AND ar.rater_id = $2 + -- Assert user is current member of the organisation that owns the + -- campaign this application belongs to. + AND om.user_id = $2 + ) + "#, + rating_id, + user_id + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if !is_admin { + return Err(ChaosError::Unauthorized); + } + + Ok(()) } From 3af461c8da4fcaee2605b7758c91403269ded29a Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Mon, 11 Nov 2024 14:32:42 +1100 Subject: [PATCH 05/14] fixed create rating authorisation --- backend/server/src/models/auth.rs | 44 ++++++++++++++++++++++++++- backend/server/src/service/ratings.rs | 37 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index a462bc7b..91841ef2 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -5,7 +5,7 @@ use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_admin; use crate::service::ratings::{ assert_user_is_application_reviewer_admin_given_application_id, - assert_user_is_application_reviewer_admin_given_rating_id, + assert_user_is_application_reviewer_admin_given_rating_id, assert_user_is_organisation_member, assert_user_is_rating_creator_and_organisation_member, }; use axum::extract::{FromRef, FromRequestParts, Path}; @@ -201,6 +201,48 @@ where } } +/// Get the application reviewer given a path that contains the application id. +pub struct ApplicationCreatorAdminGivenApplicationId { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for ApplicationCreatorAdminGivenApplicationId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. + let app_state = AppState::from_ref(state); + let decoding_key = &app_state.decoding_key; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| ChaosError::NotLoggedIn)?; + + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + + let pool = &app_state.db; + let user_id = claims.sub; + + let Path(application_id) = parts + .extract::>() + .await + .map_err(|_| ChaosError::BadRequest)?; + + assert_user_is_organisation_member(user_id, application_id, pool).await?; + + Ok(ApplicationCreatorAdminGivenApplicationId { user_id }) + } +} + /// Get the application reviewer given a path that contains the rating id. pub struct ApplicationReviewerAdminGivenRatingId { pub user_id: i64, diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs index 1d011ae1..da69d436 100644 --- a/backend/server/src/service/ratings.rs +++ b/backend/server/src/service/ratings.rs @@ -125,3 +125,40 @@ pub async fn assert_user_is_rating_creator_and_organisation_member( Ok(()) } + +/// Assert the given user a current member of the organisation the application is +/// associated with. +pub async fn assert_user_is_organisation_member( + user_id: i64, + application_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let is_admin = sqlx::query!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM organisation_members om + JOIN campaigns c ON om.organisation_id = c.organisation_id + JOIN applications a ON a.campaign_id = c.id + -- Find the organisation that this application rating belongs to + -- (via the campaign the rating belongs to). + WHERE a.id = $1 + -- Assert user is current member of the organisation that owns the + -- campaign this application belongs to. + AND om.user_id = $2 + ) + "#, + application_id, + user_id + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if !is_admin { + return Err(ChaosError::Unauthorized); + } + + Ok(()) +} From 5cd344e0ecab0ef6182bb613f945f8efe1ccd5b5 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 11 Nov 2024 21:58:55 +1100 Subject: [PATCH 06/14] Update .gitignore --- backend/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 5d9dc7ef..876a49bb 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -2,4 +2,5 @@ target Cargo.lock prisma-cli/prisma/migrations -/.idea \ No newline at end of file +/.idea +.DS_Store \ No newline at end of file From 5ed0c708753dcb8162b64dbcdc17ff2756df2e1c Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 11 Nov 2024 22:03:47 +1100 Subject: [PATCH 07/14] add optional `comment` to rating --- .../20240406031915_create_applications.sql | 1 + backend/server/src/models/ratings.rs | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index f80f3a6d..000cd9a7 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -95,6 +95,7 @@ CREATE TABLE application_ratings ( application_id BIGINT NOT NULL, rater_id BIGINT NOT NULL, rating INTEGER NOT NULL, + comment TEXT, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_application_ratings_applications diff --git a/backend/server/src/models/ratings.rs b/backend/server/src/models/ratings.rs index dcf1ab24..de01838f 100644 --- a/backend/server/src/models/ratings.rs +++ b/backend/server/src/models/ratings.rs @@ -11,6 +11,7 @@ pub struct Rating { pub application_id: i64, pub rater_user_id: i64, pub rating: i32, + pub comment: Option, pub created_at: DateTime, pub updated_at: DateTime, } @@ -19,6 +20,7 @@ pub struct Rating { pub struct NewRating { pub rater_user_id: i64, pub rating: i32, + pub comment: Option, } #[derive(Deserialize, Serialize)] @@ -27,6 +29,7 @@ pub struct RatingDetails { pub rater_id: i64, pub rater_name: String, pub rating: i32, + pub comment: Option, pub updated_at: DateTime, } @@ -46,16 +49,18 @@ impl Rating { let rating_id = snowflake_generator.generate(); let rater_user_id = new_rating.rater_user_id; let rating = new_rating.rating; + let comment = new_rating.comment; sqlx::query!( " - INSERT INTO application_ratings (id, application_id, rater_id, rating) - VALUES ($1, $2, $3, $4) + INSERT INTO application_ratings (id, application_id, rater_id, rating, comment) + VALUES ($1, $2, $3, $4, $5) ", rating_id, application_id, rater_user_id, - rating + rating, + comment ) .execute(transaction.deref_mut()) .await?; @@ -70,17 +75,19 @@ impl Rating { transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { let rating = updated_rating.rating; + let comment = updated_rating.comment; let current_time = Utc::now(); let _ = sqlx::query!( " UPDATE application_ratings - SET rating = $2, updated_at = $3 + SET rating = $2, comment = $3, updated_at = $4 WHERE id = $1 RETURNING id; ", rating_id, rating, + comment, current_time ) .fetch_one(transaction.deref_mut()) @@ -96,7 +103,7 @@ impl Rating { let rating = sqlx::query_as!( RatingDetails, " - SELECT r.id, rater_id, u.name as rater_name, r.rating, r.updated_at + SELECT r.id, rater_id, u.name as rater_name, r.rating, r.comment, r.updated_at FROM application_ratings r JOIN users u ON u.id = r.id WHERE r.id = $1 @@ -117,7 +124,7 @@ impl Rating { let ratings = sqlx::query_as!( RatingDetails, " - SELECT r.id, rater_id, u.name as rater_name, r.rating, r.updated_at + SELECT r.id, rater_id, u.name as rater_name, r.rating, r.comment, r.updated_at FROM application_ratings r JOIN users u ON u.id = r.id WHERE r.application_id = $1 From d7b585171a8eacdd65bf40f65f421454aa4e345f Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Mon, 11 Nov 2024 23:18:31 +1100 Subject: [PATCH 08/14] removed redundant authorisation service --- backend/server/src/handler/ratings.rs | 7 +++-- backend/server/src/models/auth.rs | 11 +++----- backend/server/src/service/ratings.rs | 39 --------------------------- 3 files changed, 7 insertions(+), 50 deletions(-) diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index 05d4658b..38d9f26d 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -1,7 +1,7 @@ use crate::models::app::AppState; use crate::models::auth::{ - ApplicationReviewerAdminGivenApplicationId, ApplicationReviewerAdminGivenRatingId, - RatingCreatorAdmin, SuperUser, + ApplicationCreatorAdminGivenApplicationId, ApplicationReviewerAdminGivenApplicationId, + ApplicationReviewerAdminGivenRatingId, RatingCreatorAdmin, SuperUser, }; use crate::models::error::ChaosError; use crate::models::ratings::{NewRating, Rating}; @@ -17,8 +17,7 @@ impl RatingsHandler { pub async fn create_rating( State(state): State, Path(application_id): Path, - // TODO: Potential bug: the check whether user is allowed to access rating doesn't make sense here, because no rating exists yet - _admin: ApplicationReviewerAdminGivenApplicationId, + _admin: ApplicationCreatorAdminGivenApplicationId, mut transaction: DBTransaction<'_>, Json(new_rating): Json, ) -> Result { diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 91841ef2..6b0aabbd 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -4,7 +4,6 @@ use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_admin; use crate::service::ratings::{ - assert_user_is_application_reviewer_admin_given_application_id, assert_user_is_application_reviewer_admin_given_rating_id, assert_user_is_organisation_member, assert_user_is_rating_creator_and_organisation_member, }; @@ -154,6 +153,9 @@ where // couldn't figure out how to dynamically check whether the id passed in path // was a rating id or an application id. +// TODO: there is currently no diff between ApplicationReviewerAdminGivenApplicationId +// and ApplicationCreatorAdminGivenApplicationId, but that might change, so separating just in case. + /// Get the application reviewer given a path that contains the application id. pub struct ApplicationReviewerAdminGivenApplicationId { pub user_id: i64, @@ -190,12 +192,7 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_application_reviewer_admin_given_application_id( - user_id, - application_id, - pool, - ) - .await?; + assert_user_is_organisation_member(user_id, application_id, pool).await?; Ok(ApplicationReviewerAdminGivenApplicationId { user_id }) } diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs index da69d436..f0381614 100644 --- a/backend/server/src/service/ratings.rs +++ b/backend/server/src/service/ratings.rs @@ -7,45 +7,6 @@ use sqlx::{Pool, Postgres, Transaction}; use std::ops::DerefMut; use uuid::Uuid; -/// Any member of the organisation that owns the campaign is an application -/// viewer, because all members are either directors or execs (TODO: might be -/// changed in the future). -pub async fn assert_user_is_application_reviewer_admin_given_application_id( - user_id: i64, - application_id: i64, - pool: &Pool, -) -> Result<(), ChaosError> { - let is_admin = sqlx::query!( - r#" - SELECT EXISTS ( - SELECT 1 - FROM organisation_members om - JOIN campaigns c ON om.organisation_id = c.organisation_id - JOIN applications a ON a.campaign_id = c.id - JOIN application_ratings ar ON a.campaign_id = c.id - -- Find the organisation that this application rating belongs to - -- (via the campaign the rating belongs to). - WHERE a.id = $1 - -- Assert user is member of the organisation that owns the campaign - -- this application belongs to. - AND om.user_id = $2 - ) - "#, - application_id, - user_id - ) - .fetch_one(pool) - .await? - .exists - .expect("`exists` should always exist in this query result"); - - if !is_admin { - return Err(ChaosError::Unauthorized); - } - - Ok(()) -} - /// Any member of the organisation that owns the campaign is an application /// viewer, because all members are either directors or execs (TODO: might be /// changed in the future). From 313c1ab374d4c2cd6a30d101732bd7edc445ed22 Mon Sep 17 00:00:00 2001 From: fritzrehde Date: Mon, 11 Nov 2024 23:26:09 +1100 Subject: [PATCH 09/14] renamed RatingCreatorAdmin --- backend/server/src/handler/ratings.rs | 4 ++-- backend/server/src/models/auth.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index 38d9f26d..40a87fe1 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -1,7 +1,7 @@ use crate::models::app::AppState; use crate::models::auth::{ ApplicationCreatorAdminGivenApplicationId, ApplicationReviewerAdminGivenApplicationId, - ApplicationReviewerAdminGivenRatingId, RatingCreatorAdmin, SuperUser, + ApplicationReviewerAdminGivenRatingId, RatingCreator, SuperUser, }; use crate::models::error::ChaosError; use crate::models::ratings::{NewRating, Rating}; @@ -35,7 +35,7 @@ impl RatingsHandler { pub async fn update( State(_state): State, Path(rating_id): Path, - _admin: RatingCreatorAdmin, + _admin: RatingCreator, mut transaction: DBTransaction<'_>, Json(updated_rating): Json, ) -> Result { diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 6b0aabbd..8089b839 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -282,12 +282,12 @@ where } } -pub struct RatingCreatorAdmin { +pub struct RatingCreator { pub user_id: i64, } #[async_trait] -impl FromRequestParts for RatingCreatorAdmin +impl FromRequestParts for RatingCreator where AppState: FromRef, S: Send + Sync, @@ -319,6 +319,6 @@ where assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, pool).await?; - Ok(RatingCreatorAdmin { user_id }) + Ok(RatingCreator { user_id }) } } From 95e4564004307053c4d7925dedb48b92baee2adc Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 12 Nov 2024 14:59:46 +1100 Subject: [PATCH 10/14] remove "Admin" from some AuthZ models --- backend/server/src/handler/ratings.rs | 12 ++++++------ backend/server/src/models/auth.rs | 22 +++++++++++----------- backend/server/src/service/ratings.rs | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/server/src/handler/ratings.rs b/backend/server/src/handler/ratings.rs index 40a87fe1..d8d6071f 100644 --- a/backend/server/src/handler/ratings.rs +++ b/backend/server/src/handler/ratings.rs @@ -1,7 +1,7 @@ use crate::models::app::AppState; use crate::models::auth::{ - ApplicationCreatorAdminGivenApplicationId, ApplicationReviewerAdminGivenApplicationId, - ApplicationReviewerAdminGivenRatingId, RatingCreator, SuperUser, + ApplicationCreatorGivenApplicationId, ApplicationReviewerGivenApplicationId, + ApplicationReviewerGivenRatingId, RatingCreator, SuperUser, }; use crate::models::error::ChaosError; use crate::models::ratings::{NewRating, Rating}; @@ -17,7 +17,7 @@ impl RatingsHandler { pub async fn create_rating( State(state): State, Path(application_id): Path, - _admin: ApplicationCreatorAdminGivenApplicationId, + _admin: ApplicationCreatorGivenApplicationId, mut transaction: DBTransaction<'_>, Json(new_rating): Json, ) -> Result { @@ -47,7 +47,7 @@ impl RatingsHandler { pub async fn get_ratings_for_application( State(_state): State, Path(application_id): Path, - _admin: ApplicationReviewerAdminGivenApplicationId, + _admin: ApplicationReviewerGivenApplicationId, mut transaction: DBTransaction<'_>, ) -> Result { let ratings = @@ -60,7 +60,7 @@ impl RatingsHandler { pub async fn get( State(_state): State, Path(rating_id): Path, - _admin: ApplicationReviewerAdminGivenRatingId, + _admin: ApplicationReviewerGivenRatingId, mut transaction: DBTransaction<'_>, ) -> Result { let org = Rating::get_rating(rating_id, &mut transaction.tx).await?; @@ -71,7 +71,7 @@ impl RatingsHandler { pub async fn delete( State(_state): State, Path(rating_id): Path, - _admin: ApplicationReviewerAdminGivenRatingId, + _admin: ApplicationReviewerGivenRatingId, mut transaction: DBTransaction<'_>, ) -> Result { Rating::delete(rating_id, &mut transaction.tx).await?; diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 8089b839..80bd026c 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -4,7 +4,7 @@ use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_admin; use crate::service::ratings::{ - assert_user_is_application_reviewer_admin_given_rating_id, assert_user_is_organisation_member, + assert_user_is_application_reviewer_given_rating_id, assert_user_is_organisation_member, assert_user_is_rating_creator_and_organisation_member, }; use axum::extract::{FromRef, FromRequestParts, Path}; @@ -157,12 +157,12 @@ where // and ApplicationCreatorAdminGivenApplicationId, but that might change, so separating just in case. /// Get the application reviewer given a path that contains the application id. -pub struct ApplicationReviewerAdminGivenApplicationId { +pub struct ApplicationReviewerGivenApplicationId { pub user_id: i64, } #[async_trait] -impl FromRequestParts for ApplicationReviewerAdminGivenApplicationId +impl FromRequestParts for ApplicationReviewerGivenApplicationId where AppState: FromRef, S: Send + Sync, @@ -194,17 +194,17 @@ where assert_user_is_organisation_member(user_id, application_id, pool).await?; - Ok(ApplicationReviewerAdminGivenApplicationId { user_id }) + Ok(ApplicationReviewerGivenApplicationId { user_id }) } } /// Get the application reviewer given a path that contains the application id. -pub struct ApplicationCreatorAdminGivenApplicationId { +pub struct ApplicationCreatorGivenApplicationId { pub user_id: i64, } #[async_trait] -impl FromRequestParts for ApplicationCreatorAdminGivenApplicationId +impl FromRequestParts for ApplicationCreatorGivenApplicationId where AppState: FromRef, S: Send + Sync, @@ -236,17 +236,17 @@ where assert_user_is_organisation_member(user_id, application_id, pool).await?; - Ok(ApplicationCreatorAdminGivenApplicationId { user_id }) + Ok(ApplicationCreatorGivenApplicationId { user_id }) } } /// Get the application reviewer given a path that contains the rating id. -pub struct ApplicationReviewerAdminGivenRatingId { +pub struct ApplicationReviewerGivenRatingId { pub user_id: i64, } #[async_trait] -impl FromRequestParts for ApplicationReviewerAdminGivenRatingId +impl FromRequestParts for ApplicationReviewerGivenRatingId where AppState: FromRef, S: Send + Sync, @@ -276,9 +276,9 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_application_reviewer_admin_given_rating_id(user_id, rating_id, pool).await?; + assert_user_is_application_reviewer_given_rating_id(user_id, rating_id, pool).await?; - Ok(ApplicationReviewerAdminGivenRatingId { user_id }) + Ok(ApplicationReviewerGivenRatingId { user_id }) } } diff --git a/backend/server/src/service/ratings.rs b/backend/server/src/service/ratings.rs index f0381614..0ff6b095 100644 --- a/backend/server/src/service/ratings.rs +++ b/backend/server/src/service/ratings.rs @@ -10,7 +10,7 @@ use uuid::Uuid; /// Any member of the organisation that owns the campaign is an application /// viewer, because all members are either directors or execs (TODO: might be /// changed in the future). -pub async fn assert_user_is_application_reviewer_admin_given_rating_id( +pub async fn assert_user_is_application_reviewer_given_rating_id( user_id: i64, rating_id: i64, pool: &Pool, From 1066bdd43be1454a6287fc07b59c02a08a41d83a Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 12 Nov 2024 18:02:29 +1100 Subject: [PATCH 11/14] fix merge error --- backend/server/src/main.rs | 2 +- backend/server/src/models/auth.rs | 5 ++--- backend/server/src/service/organisation.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 46942d42..1acdfa51 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -5,7 +5,7 @@ use crate::handler::organisation::OrganisationHandler; use crate::handler::application::ApplicationHandler; use crate::models::storage::Storage; use anyhow::Result; -use axum::routing::{delete, get, patch, post, put}; +use axum::routing::{get, patch, post}; use axum::Router; use handler::ratings::RatingsHandler; use handler::role::RoleHandler; diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 105893d2..89e82277 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -5,12 +5,11 @@ use crate::service::application::user_is_application_admin; use crate::service::auth::is_super_user; use crate::service::campaign::user_is_campaign_admin; use crate::service::jwt::decode_auth_token; -use crate::service::organisation::assert_user_is_admin; +use crate::service::organisation::assert_user_is_organisation_admin; use crate::service::ratings::{ assert_user_is_application_reviewer_given_rating_id, assert_user_is_organisation_member, assert_user_is_rating_creator_and_organisation_member, }; -use crate::service::organisation::user_is_organisation_admin; use crate::service::role::user_is_role_admin; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; @@ -153,7 +152,7 @@ where .get("organisation_id") .ok_or(ChaosError::BadRequest)?; - user_is_organisation_admin(user_id, organisation_id, pool).await?; + assert_user_is_organisation_admin(user_id, organisation_id, pool).await?; Ok(OrganisationAdmin { user_id }) } diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index d15e8efc..cf7f0e5e 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,7 +1,7 @@ use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; -pub async fn assert_user_is_admin( +pub async fn assert_user_is_organisation_admin( user_id: i64, organisation_id: i64, pool: &Pool, From 1c94905437ac5d9df839d44e839edab7f0325a65 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 12 Nov 2024 18:06:44 +1100 Subject: [PATCH 12/14] use `ChaosError` for Question and Answer functions --- backend/server/src/models/answer.rs | 9 ++++----- backend/server/src/models/question.rs | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index f79bf9d6..b892fc18 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -1,5 +1,4 @@ use crate::models::error::ChaosError; -use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, QueryBuilder, Row}; @@ -44,17 +43,17 @@ pub enum AnswerData { } impl AnswerData { - pub async fn validate(self) -> Result<()> { + pub async fn validate(self) -> Result<(), ChaosError> { match self { - Self::ShortAnswer(text) => if text.len() == 0 { bail!("Empty answer") }, - Self::MultiSelect(data) => if data.len() == 0 { bail!("Empty answer") }, + Self::ShortAnswer(text) => if text.len() == 0 { return Err(ChaosError::BadRequest) }, + Self::MultiSelect(data) => if data.len() == 0 { return Err(ChaosError::BadRequest) }, _ => {}, } Ok(()) } - pub async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result<()> { + pub async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result<(), ChaosError> { match self { Self::ShortAnswer(text) => { let result = sqlx::query!( diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 307a7aae..27eb5e85 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -1,5 +1,4 @@ use crate::models::error::ChaosError; -use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, QueryBuilder, Row}; @@ -87,7 +86,7 @@ pub struct MultiOptionQuestionOption { } impl QuestionData { - pub async fn validate(self) -> Result<()> { + pub async fn validate(self) -> Result<(), ChaosError> { match self { Self::ShortAnswer => Ok(()), Self::MultiChoice(data) @@ -98,12 +97,12 @@ impl QuestionData { return Ok(()); }; - bail!("Invalid number of options.") + Err(ChaosError::BadRequest) } } } - pub async fn insert_into_db(self, question_id: i64, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator) -> Result<()> { + pub async fn insert_into_db(self, question_id: i64, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> { match self { Self::ShortAnswer => Ok(()), Self::MultiChoice(data) From 9a8c7b79f268eea5031a33f185c11846a9c13560 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 12 Nov 2024 18:10:20 +1100 Subject: [PATCH 13/14] Update Cargo.toml --- backend/server/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 9c4babe2..70368407 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -16,13 +16,13 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chron anyhow = "1.0" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.12", features = ["json"] } serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } oauth2 = "4.4" log = "0.4" uuid = { version = "1.5", features = ["serde", "v4"] } -rust-s3 = "0.34.0" +rust-s3 = "0.35" rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" From 2ddb0e3fbbce494531490781d67df3a3dde020fd Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 12 Nov 2024 18:20:50 +1100 Subject: [PATCH 14/14] Revert "Update Cargo.toml" This reverts commit 9a8c7b79f268eea5031a33f185c11846a9c13560. --- backend/server/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 70368407..9c4babe2 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -16,13 +16,13 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chron anyhow = "1.0" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.11", features = ["json"] } serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } oauth2 = "4.4" log = "0.4" uuid = { version = "1.5", features = ["serde", "v4"] } -rust-s3 = "0.35" +rust-s3 = "0.34.0" rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15"