From 018124f7b310f1f44c70dbb342e5b01e9f5a5995 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Tue, 7 Oct 2025 13:08:23 +0700 Subject: [PATCH] feat: Implement permissions checks for hackathon and testimonial routes, enhancing security with header validation --- Cargo.lock | 1 + .../v1/landing/events/events_controller.rs | 39 ++++++++++------ .../testimonials/testimonials_controller.rs | 46 ++++++++++++------- imphnen-gateway/src/docs.rs | 42 +++++++++++++++-- imphnen-hackathon/Cargo.toml | 1 + .../src/v1/hackathon/hackathon_controller.rs | 37 ++++++++++++--- 6 files changed, 126 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e49ba8..915a478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2202,6 +2202,7 @@ dependencies = [ "dotenvy", "http-body-util", "imphnen-entities", + "imphnen-iam", "imphnen-libs", "imphnen-utils", "lazy_static", diff --git a/imphnen-cms/src/v1/landing/events/events_controller.rs b/imphnen-cms/src/v1/landing/events/events_controller.rs index 4ffaaeb..499d831 100644 --- a/imphnen-cms/src/v1/landing/events/events_controller.rs +++ b/imphnen-cms/src/v1/landing/events/events_controller.rs @@ -7,11 +7,12 @@ use super::{ }; use axum::extract::{Path, Query}; use axum::response::IntoResponse; -use axum::{Extension, Json}; +use axum::{Extension, Json, http::HeaderMap}; use imphnen_libs::{ - AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, - ResponseSuccessDto, + AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, + ResponseSuccessDto, }; +use imphnen_iam::{PermissionsEnum, permissions_guard}; #[utoipa::path( get, @@ -68,10 +69,14 @@ pub async fn get_event_by_id( tag = "Events" )] pub async fn post_create_event( - Extension(state): Extension, - Json(payload): Json, + headers: HeaderMap, + Extension(state): Extension, + Json(payload): Json, ) -> impl IntoResponse { - EventsService::create_event(&state, payload).await + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => EventsService::create_event(&state, payload).await, + Err(response) => response, + } } #[utoipa::path( @@ -90,11 +95,15 @@ pub async fn post_create_event( tag = "Events" )] pub async fn patch_update_event( - Extension(state): Extension, - Path(id): Path, - Json(payload): Json, + headers: HeaderMap, + Extension(state): Extension, + Path(id): Path, + Json(payload): Json, ) -> impl IntoResponse { - EventsService::update_event(&state, id, payload).await + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => EventsService::update_event(&state, id, payload).await, + Err(response) => response, + } } #[utoipa::path( @@ -112,8 +121,12 @@ pub async fn patch_update_event( tag = "Events" )] pub async fn delete_event( - Extension(state): Extension, - Path(id): Path, + headers: HeaderMap, + Extension(state): Extension, + Path(id): Path, ) -> impl IntoResponse { - EventsService::delete_event(&state, id).await + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => EventsService::delete_event(&state, id).await, + Err(response) => response, + } } diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs index 2746095..c279b58 100644 --- a/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs @@ -7,12 +7,13 @@ use super::{ }; use axum::extract::{Path, Query}; use axum::response::IntoResponse; -use axum::{Extension, Json}; +use axum::{Extension, Json, http::HeaderMap}; use imphnen_iam::UsersDetailQueryDto; use imphnen_libs::{ - AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, - ResponseSuccessDto, + AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, + ResponseSuccessDto, }; +use imphnen_iam::permissions_guard; #[utoipa::path( get, @@ -69,11 +70,15 @@ pub async fn get_testimonial_by_id( tag = "Testimonials" )] pub async fn post_create_testimonial( - Extension(state): Extension, - Extension(authenticated_user): Extension, - Json(payload): Json, + headers: HeaderMap, + Extension(state): Extension, + Extension(authenticated_user): Extension, + Json(payload): Json, ) -> impl IntoResponse { - TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await + match permissions_guard(headers, Extension(state), vec![]).await { + Ok((_claims, state)) => TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await, + Err(response) => response, + } } #[utoipa::path( @@ -92,13 +97,16 @@ pub async fn post_create_testimonial( tag = "Testimonials" )] pub async fn patch_update_testimonial( - Path(id): Path, - Extension(state): Extension, - Extension(authenticated_user): Extension, - Json(payload): Json, + headers: HeaderMap, + Path(id): Path, + Extension(state): Extension, + Extension(authenticated_user): Extension, + Json(payload): Json, ) -> impl IntoResponse { - TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user) - .await + match permissions_guard(headers, Extension(state), vec![]).await { + Ok((_claims, state)) => TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await, + Err(response) => response, + } } #[utoipa::path( @@ -116,9 +124,13 @@ pub async fn patch_update_testimonial( tag = "Testimonials" )] pub async fn delete_testimonial( - Extension(state): Extension, - Extension(authenticated_user): Extension, - Path(id): Path, + headers: HeaderMap, + Extension(state): Extension, + Extension(authenticated_user): Extension, + Path(id): Path, ) -> impl IntoResponse { - TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await + match permissions_guard(headers, Extension(state), vec![]).await { + Ok((_claims, state)) => TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await, + Err(response) => response, + } } diff --git a/imphnen-gateway/src/docs.rs b/imphnen-gateway/src/docs.rs index 3d12094..f558442 100644 --- a/imphnen-gateway/src/docs.rs +++ b/imphnen-gateway/src/docs.rs @@ -40,8 +40,8 @@ use imphnen_iam::v1::teams::teams_dto::{TeamsCreateRequestDto, TeamsUpdateReques use imphnen_iam::v1::{auth, permissions, roles, users, teams}; use imphnen_iam::v1::users::users_controller::FileUploadSchema; use utoipa::{ - Modify, OpenApi, - openapi::security::{Http, HttpAuthScheme, SecurityScheme}, + Modify, OpenApi, + openapi::security::{Http, HttpAuthScheme, SecurityScheme, SecurityRequirement}, }; #[derive(OpenApi)] @@ -267,7 +267,43 @@ impl Modify for SecurityAddon { SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), ); } - } + + // Walk all paths and add a Bearer security requirement to any operation + // that declares 401 or 403 responses. This helps ensure protected + // endpoints are shown with the Bearer lock in the generated docs + // without having to annotate every controller manually. + let paths = &mut openapi.paths; + for (_path, path_item) in paths.paths.iter_mut() { + // helper to process each possible operation on the path + let mut process_op = |op: &mut Option| { + if let Some(operation) = op.as_mut() { + let mut has_auth_response = false; + let responses = &operation.responses.responses; + for status in responses.keys() { + if status == "401" || status == "403" { + has_auth_response = true; + break; + } + } + if has_auth_response { + // assign security requirement for Bearer if not already present + if operation.security.is_none() { + operation.security = Some(vec![SecurityRequirement::new::<&str, Vec<&str>, &str>("Bearer", vec![])]); + } + } + } + }; + + process_op(&mut path_item.get); + process_op(&mut path_item.post); + process_op(&mut path_item.put); + process_op(&mut path_item.patch); + process_op(&mut path_item.delete); + process_op(&mut path_item.options); + process_op(&mut path_item.head); + process_op(&mut path_item.trace); + } + } } pub fn docs_router() -> utoipa::openapi::OpenApi { diff --git a/imphnen-hackathon/Cargo.toml b/imphnen-hackathon/Cargo.toml index 872ef7e..61398da 100644 --- a/imphnen-hackathon/Cargo.toml +++ b/imphnen-hackathon/Cargo.toml @@ -8,6 +8,7 @@ imphnen-libs.workspace = true imphnen-utils.workspace = true imphnen-entities.workspace = true +imphnen-iam.workspace = true async-trait.workspace = true axum.workspace = true serde.workspace = true diff --git a/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs b/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs index 5d794d0..4b1d039 100644 --- a/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs +++ b/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs @@ -14,10 +14,15 @@ use axum::{ response::IntoResponse, routing::{delete, get, post, put}, }; +use axum::http::HeaderMap; +use imphnen_iam::{PermissionsEnum, permissions_guard}; // Hackathon routes #[utoipa::path( post, + security( + ("Bearer" = []) + ), path = "/v1/hackathons", request_body = HackathonCreateRequestDto, responses( @@ -28,12 +33,16 @@ use axum::{ tag = "Hackathons" )] pub async fn create_hackathon( + headers: HeaderMap, Extension(state): Extension, Json(payload): Json, ) -> impl IntoResponse { - match HackathonService::create_hackathon(payload, &state).await { + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => match HackathonService::create_hackathon(payload, &state).await { Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(), Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(), + }, + Err(response) => response, } } @@ -90,6 +99,9 @@ pub async fn list_hackathons( #[utoipa::path( put, + security( + ("Bearer" = []) + ), path = "/v1/hackathons/{id}", params( ("id" = String, Path, description = "Hackathon ID") @@ -104,18 +116,25 @@ pub async fn list_hackathons( tag = "Hackathons" )] pub async fn update_hackathon( + headers: HeaderMap, Extension(state): Extension, Path(id): Path, Json(payload): Json, ) -> impl IntoResponse { - match HackathonService::update_hackathon(id, payload, &state).await { - Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(), - Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(), + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => match HackathonService::update_hackathon(id, payload, &state).await { + Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(), + Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(), + }, + Err(response) => response, } } #[utoipa::path( delete, + security( + ("Bearer" = []) + ), path = "/v1/hackathons/{id}", params( ("id" = String, Path, description = "Hackathon ID") @@ -128,12 +147,16 @@ pub async fn update_hackathon( tag = "Hackathons" )] pub async fn delete_hackathon( + headers: HeaderMap, Extension(state): Extension, Path(id): Path, ) -> impl IntoResponse { - match HackathonService::delete_hackathon(id, &state).await { - Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(), - Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(), + match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await { + Ok((_claims, state)) => match HackathonService::delete_hackathon(id, &state).await { + Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(), + Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(), + }, + Err(response) => response, } }