feat: Implement permissions checks for hackathon and testimonial routes, enhancing security with header validation
This commit is contained in:
Generated
+1
@@ -2202,6 +2202,7 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
|
"imphnen-iam",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, Json, http::HeaderMap};
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -68,10 +69,14 @@ pub async fn get_event_by_id(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_event(
|
pub async fn post_create_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Json(payload): Json<EventsCreateRequestDto>,
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<EventsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(
|
#[utoipa::path(
|
||||||
@@ -90,11 +95,15 @@ pub async fn post_create_event(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_event(
|
pub async fn patch_update_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<EventsUpdateRequestDto>,
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<EventsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(
|
#[utoipa::path(
|
||||||
@@ -112,8 +121,12 @@ pub async fn patch_update_event(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_event(
|
pub async fn delete_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, Json, http::HeaderMap};
|
||||||
use imphnen_iam::UsersDetailQueryDto;
|
use imphnen_iam::UsersDetailQueryDto;
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
|
use imphnen_iam::permissions_guard;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -69,11 +70,15 @@ pub async fn get_testimonial_by_id(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_testimonial(
|
pub async fn post_create_testimonial(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Json(payload): Json<TestimonialsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(
|
#[utoipa::path(
|
||||||
@@ -92,13 +97,16 @@ pub async fn post_create_testimonial(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_testimonial(
|
pub async fn patch_update_testimonial(
|
||||||
Path(id): Path<String>,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Path(id): Path<String>,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
|
match permissions_guard(headers, Extension(state), vec![]).await {
|
||||||
.await
|
Ok((_claims, state)) => TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -116,9 +124,13 @@ pub async fn patch_update_testimonial(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_testimonial(
|
pub async fn delete_testimonial(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::{auth, permissions, roles, users, teams};
|
||||||
use imphnen_iam::v1::users::users_controller::FileUploadSchema;
|
use imphnen_iam::v1::users::users_controller::FileUploadSchema;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
Modify, OpenApi,
|
Modify, OpenApi,
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme, SecurityRequirement},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -267,7 +267,43 @@ impl Modify for SecurityAddon {
|
|||||||
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
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<utoipa::openapi::path::Operation>| {
|
||||||
|
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 {
|
pub fn docs_router() -> utoipa::openapi::OpenApi {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ imphnen-libs.workspace = true
|
|||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
|
|
||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
|
imphnen-iam.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ use axum::{
|
|||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
|
|
||||||
// Hackathon routes
|
// Hackathon routes
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
path = "/v1/hackathons",
|
path = "/v1/hackathons",
|
||||||
request_body = HackathonCreateRequestDto,
|
request_body = HackathonCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
@@ -28,12 +33,16 @@ use axum::{
|
|||||||
tag = "Hackathons"
|
tag = "Hackathons"
|
||||||
)]
|
)]
|
||||||
pub async fn create_hackathon(
|
pub async fn create_hackathon(
|
||||||
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<HackathonCreateRequestDto>,
|
Json(payload): Json<HackathonCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
|
||||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).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(
|
#[utoipa::path(
|
||||||
put,
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
path = "/v1/hackathons/{id}",
|
path = "/v1/hackathons/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Hackathon ID")
|
("id" = String, Path, description = "Hackathon ID")
|
||||||
@@ -104,18 +116,25 @@ pub async fn list_hackathons(
|
|||||||
tag = "Hackathons"
|
tag = "Hackathons"
|
||||||
)]
|
)]
|
||||||
pub async fn update_hackathon(
|
pub async fn update_hackathon(
|
||||||
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<HackathonUpdateRequestDto>,
|
Json(payload): Json<HackathonUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match HackathonService::update_hackathon(id, payload, &state).await {
|
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
||||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
Ok((_claims, state)) => match HackathonService::update_hackathon(id, payload, &state).await {
|
||||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
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(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
path = "/v1/hackathons/{id}",
|
path = "/v1/hackathons/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Hackathon ID")
|
("id" = String, Path, description = "Hackathon ID")
|
||||||
@@ -128,12 +147,16 @@ pub async fn update_hackathon(
|
|||||||
tag = "Hackathons"
|
tag = "Hackathons"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_hackathon(
|
pub async fn delete_hackathon(
|
||||||
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match HackathonService::delete_hackathon(id, &state).await {
|
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
||||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
Ok((_claims, state)) => match HackathonService::delete_hackathon(id, &state).await {
|
||||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user