Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for request guards #47

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ members = [
"okapi",
"rocket-okapi",
"rocket-okapi-codegen",
"examples/json-web-api"
"examples/json-web-api",
"examples/secure_request_guard"
]
4 changes: 4 additions & 0 deletions examples/secure_request_guard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
/.idea
13 changes: 13 additions & 0 deletions examples/secure_request_guard/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "secure_request_guard"
version = "0.1.0"
authors = ["Kristoffer Ödmark <kristoffer.odmark90@gmail.com>"]
edition = "2018"

[dependencies]
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
schemars = { version = "0.8"}
okapi = { version = "0.6.0-alpha-1", path = "../../okapi" }
rocket_okapi = { version = "0.7.0-alpha-1", path = "../../rocket-okapi" }
serde = "1.0"
tokio = "1.6"
8 changes: 8 additions & 0 deletions examples/secure_request_guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Secure Request Guard

A simple web API written using [Rocket](https://rocket.rs/) including openapi. It contains one route that needs the correct key provided
to allow access. This example implements the security called by an api key that needs to be included
in the header from the following: [apikeys] (https://swagger.io/docs/specification/authentication/api-keys/)

the key is "hello"

113 changes: 113 additions & 0 deletions examples/secure_request_guard/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use okapi::openapi3::{Object, Responses, SecurityRequirement, SecurityScheme, SecuritySchemeData};
use rocket::serde::json::Json;
use rocket::{
get,
http::Status,
request::{self, FromRequest, Outcome},
Config, Response,
};
use rocket_okapi::{
gen::OpenApiGenerator,
openapi,
request::{OpenApiFromRequest, RequestHeaderInput},
response::OpenApiResponder,
routes_with_openapi,
swagger_ui::{make_swagger_ui, SwaggerUIConfig},
};

pub struct KeyAuthorize;

#[rocket::async_trait]
impl<'a> FromRequest<'a> for KeyAuthorize {
type Error = ();
async fn from_request(
request: &'a request::Request<'_>,
) -> request::Outcome<Self, Self::Error> {
// Same as in the name
let keys: Vec<_> = request.headers().get("x-api-key").collect();

// Get the key from the http header
let out = match keys.len() {
1 => {
let key = &keys[0][..];
if key == "hello" {
Outcome::Success(KeyAuthorize)
} else {
Outcome::Failure((Status::Unauthorized, ()))
}
}
_ => {
println!("wrong amount of authorization headers found");
Outcome::Failure((Status::BadRequest, ()))
}
};
out
}
}

impl<'a, 'r> OpenApiFromRequest<'a> for KeyAuthorize {
fn request_input(
_gen: &mut OpenApiGenerator,
_name: String,
) -> rocket_okapi::Result<RequestHeaderInput> {
let mut security_req = SecurityRequirement::new();
// each security requirement needs a specific key in the openapi docs
security_req.insert("example_security".into(), Vec::new());

// The scheme for the security needs to be defined as well
// https://swagger.io/docs/specification/authentication/basic-authentication/
let security_scheme = SecurityScheme {
description: Some("requires a key to access".into()),
scheme_identifier: "FixedKeyApiKeyAuth".into(),
data: SecuritySchemeData::ApiKey {
name: "x-api-key".into(),
location: "header".into(),
},
extensions: Object::default(),
};

Ok(RequestHeaderInput::Security((
security_scheme,
security_req,
)))
}
}

/// Defines the possible responses for this request guard for the openapi docs (not used yet)
impl<'a, 'r: 'a> OpenApiResponder<'a, 'r> for KeyAuthorize {
fn responses(_: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
let responses = Responses::default();
Ok(responses)
}
}

/// Returns an empty, default `Response`. Always returns `Ok`.
/// Defines the possible response for this request guard
impl<'a, 'r: 'a> rocket::response::Responder<'a, 'r> for KeyAuthorize {
fn respond_to(self, _: &rocket::request::Request<'_>) -> rocket::response::Result<'static> {
Ok(Response::new())
}
}

#[openapi]
#[get("/restricted")]
pub fn restricted(_key: KeyAuthorize) -> Json<String> {
Json("You got access here, hurray".into())
}

#[tokio::main]
async fn main() {
let rocket_config = Config::debug_default();

let e = rocket::custom(rocket_config)
.mount("/", routes_with_openapi![restricted])
.mount(
"/api/",
make_swagger_ui(&SwaggerUIConfig {
url: "/openapi.json".to_string(),
..Default::default()
}),
)
.launch()
.await;
}
8 changes: 4 additions & 4 deletions okapi/src/openapi3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ pub use schemars::schema::SchemaObject;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
type Object = Map<String, Value>;
pub type Object = Map<String, Value>;

type SecurityRequirement = Map<String, Vec<String>>;
pub type SecurityRequirement = Map<String, Vec<String>>;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "derive_json_schema", derive(JsonSchema))]
Expand Down Expand Up @@ -352,8 +352,8 @@ pub struct Header {
#[cfg_attr(feature = "derive_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct SecurityScheme {
#[serde(rename = "type")]
pub schema_type: String,
// unique name for the security scheme
pub scheme_identifier: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
Expand Down
76 changes: 74 additions & 2 deletions rocket-okapi-codegen/src/openapi_attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,46 @@ fn create_route_operation_fn(
})
}

// Request quards, checks if the items are not found in the rocket route parameters, if that is the
// case, we assume they are request guards
let mut responses = Vec::new();
responses.push(quote! {
<#return_type as ::rocket_okapi::response::OpenApiResponder>::responses(gen)?
});

let data_param_arg = route.data_param.clone().unwrap_or_else(|| String::new());
for arg_type in arg_types {
let ty = arg_type.1;
let arg = arg_type.0;

// If the items are not found in the list of path/query parameters, assume the item is a request
// guard and let them add to the openapi specification from the trait OpenApiFromRequest
// Request guards can add or define their own responses, and can thus add to the possible
// responses from an API
if route
.path_params()
.find(|item| arg == item.to_string())
.is_none()
// Verify it is not in query parameters
&& route
.query_params()
.find(|item| arg == item.to_string())
.is_none()
&& data_param_arg != arg
{
// println!("assuming request guard for: {:?}", arg);
params.push(quote! {
<#ty as ::rocket_okapi::request::OpenApiFromRequest>::request_input(gen, #arg.to_owned())?.into()
});
//TODO: implement that RequestGuards can specify the different types of responses

// Create a response for this one
// responses.push(quote! {
// <#ty as ::rocket_okapi::response::OpenApiResponder>::responses(gen)?
// })
}
}

let fn_name = get_add_operation_fn_name(&route_fn.sig.ident);
let path = route
.origin
Expand Down Expand Up @@ -148,7 +188,38 @@ fn create_route_operation_fn(
) -> ::rocket_okapi::Result<()> {
let responses = <#return_type as ::rocket_okapi::response::OpenApiResponder>::responses(gen)?;
let request_body = #request_body;
let mut parameters: Vec<::okapi::openapi3::RefOr<::okapi::openapi3::Parameter>> = vec![#(#params),*];

//###############
use ::rocket_okapi::request::RequestHeaderInput;
use ::okapi::openapi3::Parameter;
use ::okapi::openapi3::RefOr;

let request_inputs: Vec<RequestHeaderInput> = vec![#(#params),*];

let mut parameters: Vec<::okapi::openapi3::RefOr<Parameter>> = Vec::new();
use std::collections::BTreeMap as Map;
let mut security_schemes = Map::new();
for inp in request_inputs {
match inp {
RequestHeaderInput::Parameter(p) => {
parameters.push(p.into());
}
RequestHeaderInput::Security(s) => {
// Make sure to add the security scheme listing
security_schemes.insert(s.0.scheme_identifier.clone(), Vec::new());
// Add the scheme to components definition of openapi
gen.add_security_scheme(s.0.scheme_identifier.clone(), s.0.clone());
}
_ => {
}
}
}
let security = if security_schemes.is_empty() {
None
} else {
Some(vec![security_schemes])
};

// add nested lists
let parameters_nested_list: Vec<Vec<::okapi::openapi3::Parameter>> = vec![#(#params_nested_list),*];
for inner_list in parameters_nested_list{
Expand All @@ -167,8 +238,9 @@ fn create_route_operation_fn(
parameters,
summary: #title,
description: #desc,
security,
tags: vec![#(#tags),*],
..okapi::openapi3::Operation::default()
..::okapi::openapi3::Operation::default()
},
});
Ok(())
Expand Down
20 changes: 17 additions & 3 deletions rocket-okapi/src/gen.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::settings::OpenApiSettings;
use crate::OperationInfo;
use okapi::openapi3::{Components, OpenApi, Operation, PathItem};
use okapi::openapi3::{Components, OpenApi, Operation, PathItem, RefOr, SecurityScheme};
use rocket::http::Method;
use schemars::gen::SchemaGenerator;
use schemars::schema::SchemaObject;
Expand All @@ -13,6 +13,7 @@ use std::collections::HashMap;
pub struct OpenApiGenerator {
settings: OpenApiSettings,
schema_generator: SchemaGenerator,
security_schemes: Map<String, SecurityScheme>,
operations: Map<String, HashMap<Method, Operation>>,
}

Expand All @@ -23,10 +24,16 @@ impl OpenApiGenerator {
OpenApiGenerator {
schema_generator: settings.schema_settings.clone().into_generator(),
settings,
operations: okapi::Map::default(),
security_schemes: Default::default(),
operations: Default::default(),
}
}

/// Adds a security scheme to the generated output
pub fn add_security_scheme(&mut self, name: String, scheme: SecurityScheme) {
self.security_schemes.insert(name, scheme);
}

/// Add a new `HTTP Method` to the collection of endpoints in the `OpenApiGenerator`.
pub fn add_operation(&mut self, mut op: OperationInfo) {
if let Some(op_id) = op.operation.operation_id {
Expand Down Expand Up @@ -73,6 +80,12 @@ impl OpenApiGenerator {
let mut schema_generator = self.schema_generator;
let mut schemas = schema_generator.take_definitions();

// Add the security schemes
let mut schemes: Map<String, RefOr<SecurityScheme>> = Default::default();
for (name, schema) in self.security_schemes {
schemes.insert(name, schema.into());
}

for visitor in schema_generator.visitors_mut() {
for schema in schemas.values_mut() {
visitor.visit_schema(schema)
Expand All @@ -93,7 +106,8 @@ impl OpenApiGenerator {
},
components: Some(Components {
schemas: schemas.into_iter().map(|(k, v)| (k, v.into())).collect(),
..Components::default()
security_schemes: schemes,
..Default::default()
}),
..OpenApi::default()
}
Expand Down
23 changes: 23 additions & 0 deletions rocket-okapi/src/request/from_request_impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use super::{OpenApiFromRequest, RequestHeaderInput};
use crate::gen::OpenApiGenerator;
use okapi::openapi3::*;

impl<'a, T: Send + Sync> OpenApiFromRequest<'a> for &'a rocket::State<T> {
fn request_input(
_gen: &mut OpenApiGenerator,
name: String,
) -> crate::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::Parameter(Parameter {
required: false,
name: name,
location: "header".into(),
description: None,
deprecated: false,
allow_empty_value: true,
extensions: Object::default(),
value: ParameterValue::Content {
content: Default::default(),
},
}))
}
}
29 changes: 28 additions & 1 deletion rocket-okapi/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ mod from_data_impls;
mod from_form_multi_param_impls;
mod from_form_param_impls;
mod from_param_impls;
mod from_request_impls;

use super::gen::OpenApiGenerator;
use super::Result;
use okapi::openapi3::{Parameter, RequestBody};
use okapi::openapi3::{Parameter, RequestBody, SecurityRequirement, SecurityScheme};

/// Expose this to the public to be use when manualy implementing a
/// [Form Guard](https://api.rocket.rs/master/rocket/form/trait.FromForm.html).
Expand Down Expand Up @@ -58,3 +59,29 @@ pub trait OpenApiFromForm<'r>: rocket::form::FromForm<'r> {
required: bool,
) -> Result<Vec<Parameter>>;
}

/// Commonly the items in the request header can be parameters, or authorization methods in rocket,
/// this item will let the implementer choose what they are
pub enum RequestHeaderInput {
/// This request header requires no input anywhere
None,
/// Parameter input to the path
Parameter(Parameter),
/// the path implements a security scheme
Security((SecurityScheme, SecurityRequirement)),
}

impl Into<RequestHeaderInput> for Parameter {
fn into(self) -> RequestHeaderInput {
RequestHeaderInput::Parameter(self)
}
}

/// This will let the request guards add to the openapi spec
pub trait OpenApiFromRequest<'a>: rocket::request::FromRequest<'a> {
/// Return a parameter for items that are found in the function call, but are not found
/// anywhere in the path definition by rocket, defaults to a none implementation for simplicity
fn request_input(_gen: &mut OpenApiGenerator, _name: String) -> Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}