Skip to content

Commit

Permalink
SecurityAddon
Browse files Browse the repository at this point in the history
  • Loading branch information
NexVeridian committed Nov 22, 2024
1 parent abcf9ce commit 3e106d0
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 44 deletions.
43 changes: 11 additions & 32 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ pub trait Hooks: Send {
/// Modify the OpenAPI spec before the routes are added, allowing you to edit (openapi::info)[https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html]
/// # Examples
/// ```rust ignore
/// fn inital_openapi_spec() {
/// fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi {
/// #[derive(OpenApi)]
/// #[openapi(info(
/// title = "Loco Demo",
Expand All @@ -236,39 +236,18 @@ pub trait Hooks: Send {
///
/// With SecurityAddon
/// ```rust ignore
/// fn inital_openapi_spec() {
/// fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi {
/// set_jwt_location(ctx);
///
/// #[derive(OpenApi)]
/// #[openapi(modifiers(&SecurityAddon), info(
/// title = "Loco Demo",
/// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
/// ))]
/// #[openapi(
/// modifiers(&SecurityAddon),
/// info(
/// title = "Loco Demo",
/// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
/// )
/// )]
/// struct ApiDoc;
///
/// // TODO set the jwt token location
/// // let auth_location = ctx.config.auth.as_ref();
///
/// struct SecurityAddon;
/// impl Modify for SecurityAddon {
/// fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
/// if let Some(components) = openapi.components.as_mut() {
/// components.add_security_schemes_from_iter([
/// (
/// "jwt_token",
/// SecurityScheme::Http(
/// HttpBuilder::new()
/// .scheme(HttpAuthScheme::Bearer)
/// .bearer_format("JWT")
/// .build(),
/// ),
/// ),
/// (
/// "api_key",
/// SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))),
/// ),
/// ]);
/// }
/// }
/// }
/// ApiDoc::openapi()
/// }
/// ```
Expand Down
2 changes: 2 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#[cfg(feature = "auth_jwt")]
pub mod jwt;
#[cfg(feature = "openapi")]
pub mod openapi;
57 changes: 57 additions & 0 deletions src/auth/openapi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::sync::OnceLock;
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme},
Modify,
};

use crate::{app::AppContext, config::JWTLocation};

static JWT_LOCATION: OnceLock<JWTLocation> = OnceLock::new();

pub fn set_jwt_location(ctx: &AppContext) -> &'static JWTLocation {
JWT_LOCATION.get_or_init(|| {
ctx.config
.auth
.as_ref()
.and_then(|auth| auth.jwt.as_ref())
.and_then(|jwt| jwt.location.as_ref())
.unwrap_or(&JWTLocation::Bearer)
.clone()
})
}

fn get_jwt_location() -> &'static JWTLocation {
JWT_LOCATION.get().unwrap()
}

pub struct SecurityAddon;

impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_schemes_from_iter([
(
"jwt_token",
match get_jwt_location() {
JWTLocation::Bearer => SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
JWTLocation::Query { name } => {
SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(name)))
}
JWTLocation::Cookie { name } => {
SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(name)))
}
},
),
(
"api_key",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))),
),
]);
}
}
}
17 changes: 12 additions & 5 deletions src/tests_cfg/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub use sea_orm_migration::prelude::*;
#[cfg(feature = "openapi")]
use utoipa::OpenApi;

#[cfg(feature = "openapi")]
use crate::auth::openapi::{set_jwt_location, SecurityAddon};
#[cfg(feature = "channels")]
use crate::controller::channels::AppChannels;
use crate::{
Expand Down Expand Up @@ -131,12 +133,17 @@ impl Hooks for AppHook {
}

#[cfg(feature = "openapi")]
fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi {
fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi {
set_jwt_location(ctx);

#[derive(OpenApi)]
#[openapi(info(
title = "Loco Demo",
description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
))]
#[openapi(
modifiers(&SecurityAddon),
info(
title = "Loco Demo",
description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
)
)]
struct ApiDoc;
ApiDoc::openapi()
}
Expand Down
51 changes: 50 additions & 1 deletion tests/controller/openapi.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use insta::assert_debug_snapshot;
use loco_rs::{prelude::*, tests_cfg};
use loco_rs::{
config::{Auth, JWTLocation, JWT},
prelude::*,
tests_cfg,
};
use rstest::rstest;
use serial_test::serial;

Expand Down Expand Up @@ -98,3 +102,48 @@ async fn openapi_spec(#[case] test_name: &str) {

handle.abort();
}

#[rstest]
#[case(JWTLocation::Query { name: "JWT".to_string() })]
#[case(JWTLocation::Cookie { name: "JWT".to_string() })]
#[tokio::test]
#[serial]
async fn openapi_security(#[case] location: JWTLocation) {
configure_insta!();

let mut ctx: AppContext = tests_cfg::app::get_app_context().await;
ctx.config.auth = Some(Auth {
jwt: Some(JWT {
location: Some(location.clone()),
secret: "PqRwLF2rhHe8J22oBeHy".to_string(),
expiration: 604800,
}),
});

let handle = infra_cfg::server::start_from_ctx(ctx).await;

let res = reqwest::Client::new()
.request(
reqwest::Method::GET,
infra_cfg::server::get_base_url() + "api-docs/openapi.json",
)
.send()
.await
.expect("valid response");

let test_name = match location {
JWTLocation::Query { .. } => "Query",
JWTLocation::Cookie { .. } => "Cookie",
_ => "Bearer",
};
assert_debug_snapshot!(
format!("openapi_security_[{test_name}]"),
(
res.status().to_string(),
res.url().to_string(),
res.text().await.unwrap(),
)
);

handle.abort();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: tests/controller/openapi.rs
expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)"
---
(
"200 OK",
"http://localhost:5555/api-docs/openapi.json",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"cookie\",\"name\":\"JWT\"}}}}",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: tests/controller/openapi.rs
expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)"
---
(
"200 OK",
"http://localhost:5555/api-docs/openapi.json",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"query\",\"name\":\"JWT\"}}}}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await.
(
"200 OK",
"http://localhost:5555/api-docs/openapi.json",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await.
(
"200 OK",
"http://localhost:5555/api-docs/openapi.yaml",
"openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: [email protected]\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n",
"openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: [email protected]\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n",
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await.
(
"200 OK",
"http://localhost:5555/redoc/openapi.json",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await.
(
"200 OK",
"http://localhost:5555/redoc/openapi.yaml",
"openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: [email protected]\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n",
"openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: [email protected]\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n",
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await.
(
"200 OK",
"http://localhost:5555/scalar/openapi.json",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}",
"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"[email protected]\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}",
)
Loading

0 comments on commit 3e106d0

Please sign in to comment.