diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java index 3ad87d03aff..215af9d7e0e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -230,11 +230,15 @@ public class RustServerCodegen extends AbstractRustCodegen implements CodegenCon supportingFiles.add(new SupportingFile("context.mustache", "src", "context.rs")); supportingFiles.add(new SupportingFile("models.mustache", "src", "models.rs")); supportingFiles.add(new SupportingFile("header.mustache", "src", "header.rs")); + supportingFiles.add(new SupportingFile("auth.mustache", "src", "auth.rs")); supportingFiles.add(new SupportingFile("server-mod.mustache", "src/server", "mod.rs")); + supportingFiles.add(new SupportingFile("server-server_auth.mustache", "src/server", "server_auth.rs")); supportingFiles.add(new SupportingFile("client-mod.mustache", "src/client", "mod.rs")); supportingFiles.add(new SupportingFile("example-server-main.mustache", "examples/server", "main.rs")); supportingFiles.add(new SupportingFile("example-server-server.mustache", "examples/server", "server.rs")); + supportingFiles.add(new SupportingFile("example-server-auth.mustache", "examples/server", "server_auth.rs")); supportingFiles.add(new SupportingFile("example-client-main.mustache", "examples/client", "main.rs")); + supportingFiles.add(new SupportingFile("example-client-auth.mustache", "examples/client", "client_auth.rs")); supportingFiles.add(new SupportingFile("example-ca.pem", "examples", "ca.pem")); supportingFiles.add(new SupportingFile("example-server-chain.pem", "examples", "server-chain.pem")); supportingFiles.add(new SupportingFile("example-server-key.pem", "examples", "server-key.pem")); diff --git a/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache b/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache index 806211893fe..1cd664417e0 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache @@ -17,7 +17,7 @@ license = "{{.}}" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" {{/licenseInfo}} -edition = "2021" +edition = "2018" {{#publishRustRegistry}} publish = ["{{.}}"] {{/publishRustRegistry}} @@ -133,6 +133,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/modules/openapi-generator/src/main/resources/rust-server/auth.mustache b/modules/openapi-generator/src/main/resources/rust-server/auth.mustache new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/auth.mustache @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/modules/openapi-generator/src/main/resources/rust-server/context.mustache b/modules/openapi-generator/src/main/resources/rust-server/context.mustache index 4ec03e0d2e4..061bd4fbf2d 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/context.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/context.mustache @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; @@ -111,9 +112,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.basic_authorization(&basic); let auth_data = AuthData::Basic(basic); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -124,9 +133,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.bearer_authorization(&bearer); let auth_data = AuthData::Bearer(bearer); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -138,9 +155,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.bearer_authorization(&bearer); let auth_data = AuthData::Bearer(bearer); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -152,9 +177,17 @@ impl Service> for AddContext); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -167,9 +200,17 @@ impl Service> for AddContext); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-client-auth.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-client-auth.mustache new file mode 100644 index 00000000000..07b7713d820 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/example-client-auth.mustache @@ -0,0 +1,17 @@ +use {{{externCrateName}}}::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-client-main.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-client-main.mustache index b29b387b1e4..f452904d845 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-client-main.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-client-main.mustache @@ -7,7 +7,7 @@ mod server; #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use {{{externCrateName}}}::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use {{{externCrateName}}}::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, {{#apiInfo}} {{#apis}} {{#operations}} @@ -20,6 +20,9 @@ use {{{externCrateName}}}::{Api, ApiNoContext, Client, ContextWrapperExt, models }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -29,6 +32,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -44,7 +51,7 @@ fn main() { {{#operation}} {{#vendorExtensions}} {{^x-no-client-example}} - "{{{operationId}}}", + "{{{operationId}}}", {{/x-no-client-example}} {{/vendorExtensions}} {{/operation}} @@ -69,14 +76,45 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + {{#authMethods}} + {{#scopes}} + "{{{scope}}}", + {{/scopes}} + {{/authMethods}} + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-auth.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-auth.mustache new file mode 100644 index 00000000000..148012048dd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-auth.mustache @@ -0,0 +1,132 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use {{{externCrateName}}}::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + {{#authMethods}} + {{#scopes}} + "{{{scope}}}", + {{/scopes}} + {{/authMethods}} + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-common.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-common.mustache index 924abea8549..b3a823d6eaa 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server-common.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-common.mustache @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-main.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-main.mustache index c0d39f682e5..58d6f9e2a65 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server-main.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-main.mustache @@ -1,10 +1,13 @@ //! Main binary entry point for {{{externCrateName}}} implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache index 43b66ae59e3..aea5b654376 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache @@ -14,5 +14,5 @@ context: &C) -> Result<{{{operationId}}}Response, ApiError> { info!("{{#vendorExtensions}}{{{x-operation-id}}}{{/vendorExtensions}}({{#allParams}}{{#vendorExtensions}}{{{x-format-string}}}{{/vendorExtensions}}{{^-last}}, {{/-last}}{{/allParams}}) - X-Span-ID: {:?}"{{#allParams}}, {{{paramName}}}{{/allParams}}, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-server.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-server.mustache index d04e09aad88..bfa094f523c 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server-server.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-server.mustache @@ -1,5 +1,11 @@ {{>example-server-common}} +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use {{{externCrateName}}}::{ Api, {{#apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/rust-server/lib.mustache b/modules/openapi-generator/src/main/resources/rust-server/lib.mustache index cbd262e97f1..17d2f3cbaaa 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/lib.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/lib.mustache @@ -1,13 +1,16 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; @@ -16,6 +19,10 @@ pub const BASE_PATH: &str = "{{{basePathWithoutHost}}}"; pub const API_VERSION: &str = "{{{.}}}"; {{/appVersion}} +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + {{#apiInfo}} {{#apis}} {{#operations}} diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-imports.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-imports.mustache index 9aa79b47ea4..be6effa720c 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/server-imports.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/server-imports.mustache @@ -23,8 +23,7 @@ use multipart::server::save::SaveResult; {{/apiUsesMultipartFormData}} #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-make-service.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-make-service.mustache index 684406fbc03..5f5359de68a 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/server-make-service.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/server-make-service.mustache @@ -1,3 +1,4 @@ + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has {{#hasAuthMethods}}+ Has>{{/hasAuthMethods}} + Send + Sync + 'static @@ -18,6 +19,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has {{#hasAuthMethods}}+ Has>{{/hasAuthMethods}} + Send + Sync + 'static diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache index 08e63ca087e..4111630d6e3 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache @@ -3,6 +3,8 @@ use crate::{Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, {{{operationId}}}Response{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} }; +mod server_auth; + {{#hasCallbacks}} pub mod callbacks; diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache index 3bc9b31296c..f0ba616a9ab 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache @@ -224,11 +224,10 @@ let deserializer = &mut serde_json::Deserializer::from_slice(&body); {{/x-consumes-json}} {{^x-consumes-plain-text}} - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_{{{paramName}}}) => param_{{{paramName}}}, {{#required}} Err(e) => return Ok(Response::builder() @@ -422,11 +421,10 @@ Some("{{{contentType}}}") if param_{{{paramName}}}.is_none() => { // Extract JSON part. let deserializer = &mut serde_json::Deserializer::from_slice(part.body.as_slice()); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { + let json_data: {{dataType}} = match serde_ignored::deserialize(deserializer, |path| { warn!("Ignoring unknown field in JSON part: {}", path); unused_elements.push(path.to_string()); - }; - let json_data: {{dataType}} = match serde_ignored::deserialize(deserializer, handle_unknown_field) { + }) { Ok(json_data) => json_data, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-server_auth.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-server_auth.mustache new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/server-server_auth.mustache @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/multipart-v3/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/multipart-v3/.openapi-generator/FILES index f72a89817bf..c4ea8aa3665 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/multipart-v3/.openapi-generator/FILES @@ -8,14 +8,18 @@ docs/MultipartRequestObjectField.md docs/MultipleIdenticalMimeTypesPostRequest.md docs/default_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/multipart-v3/Cargo.toml b/samples/server/petstore/rust-server/output/multipart-v3/Cargo.toml index 60992d137b8..2dec3ee847f 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/Cargo.toml +++ b/samples/server/petstore/rust-server/output/multipart-v3/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "API under test" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -69,6 +69,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/client/client_auth.rs new file mode 100644 index 00000000000..ed71c3056f8 --- /dev/null +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use multipart_v3::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/client/main.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/client/main.rs index 2723cbfc98d..104343c50f4 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/client/main.rs @@ -4,13 +4,16 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use multipart_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use multipart_v3::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, MultipartRelatedRequestPostResponse, MultipartRequestPostResponse, MultipleIdenticalMimeTypesPostResponse, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -20,6 +23,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -29,9 +36,9 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "MultipartRelatedRequestPost", - "MultipartRequestPost", - "MultipleIdenticalMimeTypesPost", + "MultipartRelatedRequestPost", + "MultipartRequestPost", + "MultipleIdenticalMimeTypesPost", ]) .required(true) .index(1)) @@ -50,14 +57,40 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/main.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/main.rs index 834961ce82e..a0b4755e80c 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for multipart_v3 implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs index a33ffe55388..8eecc09eb2f 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use multipart_v3::{ Api, MultipartRelatedRequestPostResponse, @@ -113,7 +123,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("multipart_related_request_post({:?}, {:?}, {:?}) - X-Span-ID: {:?}", required_binary_field, object_field, optional_binary_field, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn multipart_request_post( @@ -125,7 +135,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("multipart_request_post(\"{}\", {:?}, {:?}, {:?}) - X-Span-ID: {:?}", string_field, binary_field, optional_string_field, object_field, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn multiple_identical_mime_types_post( @@ -135,7 +145,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("multiple_identical_mime_types_post({:?}, {:?}) - X-Span-ID: {:?}", binary1, binary2, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server_auth.rs new file mode 100644 index 00000000000..e12846a950b --- /dev/null +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server_auth.rs @@ -0,0 +1,127 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use multipart_v3::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/multipart-v3/src/auth.rs b/samples/server/petstore/rust-server/output/multipart-v3/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/multipart-v3/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/multipart-v3/src/context.rs b/samples/server/petstore/rust-server/output/multipart-v3/src/context.rs index fadd880b965..ee8e118587b 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/src/context.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; diff --git a/samples/server/petstore/rust-server/output/multipart-v3/src/lib.rs b/samples/server/petstore/rust-server/output/multipart-v3/src/lib.rs index ea2a11d63da..b10ec08a279 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/src/lib.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "1.0.7"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum MultipartRelatedRequestPostResponse { /// OK diff --git a/samples/server/petstore/rust-server/output/multipart-v3/src/server/mod.rs b/samples/server/petstore/rust-server/output/multipart-v3/src/server/mod.rs index 9bf8330d446..d8e45aac711 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/src/server/mod.rs @@ -19,8 +19,7 @@ use multipart::server::Multipart; use multipart::server::save::SaveResult; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -32,6 +31,8 @@ use crate::{Api, MultipleIdenticalMimeTypesPostResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -48,6 +49,7 @@ mod paths { pub(crate) static ID_MULTIPLE_IDENTICAL_MIME_TYPES: usize = 2; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -68,6 +70,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -206,11 +209,10 @@ impl hyper::service::Service<(Request, C)> for Service where Some("application/json") if param_object_field.is_none() => { // Extract JSON part. let deserializer = &mut serde_json::Deserializer::from_slice(part.body.as_slice()); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { + let json_data: models::MultipartRequestObjectField = match serde_ignored::deserialize(deserializer, |path| { warn!("Ignoring unknown field in JSON part: {}", path); unused_elements.push(path.to_string()); - }; - let json_data: models::MultipartRequestObjectField = match serde_ignored::deserialize(deserializer, handle_unknown_field) { + }) { Ok(json_data) => json_data, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/samples/server/petstore/rust-server/output/multipart-v3/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/multipart-v3/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/multipart-v3/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/no-example-v3/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/no-example-v3/.openapi-generator/FILES index 1bb2bdbcce9..c41dce9b6f0 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/no-example-v3/.openapi-generator/FILES @@ -6,14 +6,18 @@ api/openapi.yaml docs/OpGetRequest.md docs/default_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/no-example-v3/Cargo.toml b/samples/server/petstore/rust-server/output/no-example-v3/Cargo.toml index 94c1dfcf421..3b708112b2f 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/Cargo.toml +++ b/samples/server/petstore/rust-server/output/no-example-v3/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -59,6 +59,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/no-example-v3/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/no-example-v3/examples/client/client_auth.rs new file mode 100644 index 00000000000..415b7b82998 --- /dev/null +++ b/samples/server/petstore/rust-server/output/no-example-v3/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use no_example_v3::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/no-example-v3/examples/client/main.rs b/samples/server/petstore/rust-server/output/no-example-v3/examples/client/main.rs index ce34c07c8d5..0d53eba568b 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/examples/client/main.rs @@ -4,11 +4,14 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use no_example_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use no_example_v3::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, OpGetResponse, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -18,6 +21,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -45,14 +52,40 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/no-example-v3/examples/server/main.rs b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/main.rs index d1a25ec9b63..ae01015501e 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for no_example_v3 implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server.rs index 9ca6a4e5e41..64eb831abca 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use no_example_v3::{ Api, OpGetResponse, @@ -109,7 +119,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op_get({:?}) - X-Span-ID: {:?}", op_get_request, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server_auth.rs new file mode 100644 index 00000000000..d08ae850a01 --- /dev/null +++ b/samples/server/petstore/rust-server/output/no-example-v3/examples/server/server_auth.rs @@ -0,0 +1,127 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use no_example_v3::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/no-example-v3/src/auth.rs b/samples/server/petstore/rust-server/output/no-example-v3/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/no-example-v3/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/no-example-v3/src/context.rs b/samples/server/petstore/rust-server/output/no-example-v3/src/context.rs index fadd880b965..ee8e118587b 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/src/context.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; diff --git a/samples/server/petstore/rust-server/output/no-example-v3/src/lib.rs b/samples/server/petstore/rust-server/output/no-example-v3/src/lib.rs index 8c66235e7b7..9e60692bed3 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/src/lib.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "0.0.1"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum OpGetResponse { /// OK diff --git a/samples/server/petstore/rust-server/output/no-example-v3/src/server/mod.rs b/samples/server/petstore/rust-server/output/no-example-v3/src/server/mod.rs index 496d8929636..d00a957c053 100644 --- a/samples/server/petstore/rust-server/output/no-example-v3/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/no-example-v3/src/server/mod.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -25,6 +24,8 @@ use crate::{Api, OpGetResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -37,6 +38,7 @@ mod paths { pub(crate) static ID_OP: usize = 0; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -57,6 +59,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -150,11 +153,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_op_get_request: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_op_get_request) => param_op_get_request, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/samples/server/petstore/rust-server/output/no-example-v3/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/no-example-v3/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/no-example-v3/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES index 995168af5ff..7d5a6131394 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES @@ -37,12 +37,15 @@ docs/XmlObject.md docs/default_api.md docs/repo_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/client/server.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/callbacks.rs src/client/mod.rs src/context.rs @@ -51,3 +54,4 @@ src/lib.rs src/models.rs src/server/callbacks.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/openapi-v3/Cargo.toml b/samples/server/petstore/rust-server/output/openapi-v3/Cargo.toml index dd0059807bb..3dd0ee4d5f9 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/Cargo.toml +++ b/samples/server/petstore/rust-server/output/openapi-v3/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "API under test" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -65,6 +65,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/client_auth.rs new file mode 100644 index 00000000000..db0c36da52f --- /dev/null +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use openapi_v3::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/client/main.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/main.rs index 04b4e28dfde..af8b2f38293 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/main.rs @@ -5,7 +5,7 @@ mod server; #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use openapi_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use openapi_v3::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, AnyOfGetResponse, CallbackWithHeaderPostResponse, ComplexQueryParamGetResponse, @@ -35,6 +35,9 @@ use openapi_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -44,6 +47,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -53,31 +60,31 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "AnyOfGet", - "CallbackWithHeaderPost", - "ComplexQueryParamGet", - "JsonComplexQueryParamGet", - "MandatoryRequestHeaderGet", - "MergePatchJsonGet", - "MultigetGet", - "MultipleAuthSchemeGet", - "OneOfGet", - "OverrideServerGet", - "ParamgetGet", - "ReadonlyAuthSchemeGet", - "RegisterCallbackPost", - "RequiredOctetStreamPut", - "ResponsesWithHeadersGet", - "Rfc7807Get", - "UntypedPropertyGet", - "UuidGet", - "XmlExtraPost", - "XmlOtherPost", - "XmlOtherPut", - "XmlPost", - "XmlPut", - "CreateRepo", - "GetRepoInfo", + "AnyOfGet", + "CallbackWithHeaderPost", + "ComplexQueryParamGet", + "JsonComplexQueryParamGet", + "MandatoryRequestHeaderGet", + "MergePatchJsonGet", + "MultigetGet", + "MultipleAuthSchemeGet", + "OneOfGet", + "OverrideServerGet", + "ParamgetGet", + "ReadonlyAuthSchemeGet", + "RegisterCallbackPost", + "RequiredOctetStreamPut", + "ResponsesWithHeadersGet", + "Rfc7807Get", + "UntypedPropertyGet", + "UuidGet", + "XmlExtraPost", + "XmlOtherPost", + "XmlOtherPut", + "XmlPost", + "XmlPut", + "CreateRepo", + "GetRepoInfo", ]) .required(true) .index(1)) @@ -96,14 +103,42 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + "test.read", + "test.write", + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/client/server.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/server.rs index a7c806ff1c5..96694d1ad85 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/examples/client/server.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/client/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -108,7 +112,7 @@ impl CallbackApi for Server where C: Has + Send + Sync context: &C) -> Result { info!("callback_callback_with_header_post({:?}) - X-Span-ID: {:?}", information, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn callback_callback_post( @@ -117,7 +121,7 @@ impl CallbackApi for Server where C: Has + Send + Sync context: &C) -> Result { info!("callback_callback_post() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/main.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/main.rs index 448771feb06..08af24903e5 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for openapi_v3 implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs index d51a51417ed..63706763819 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use openapi_v3::{ Api, AnyOfGetResponse, @@ -134,7 +144,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("any_of_get({:?}) - X-Span-ID: {:?}", any_of, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn callback_with_header_post( @@ -143,7 +153,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("callback_with_header_post(\"{}\") - X-Span-ID: {:?}", url, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn complex_query_param_get( @@ -152,7 +162,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("complex_query_param_get({:?}) - X-Span-ID: {:?}", list_of_strings, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn enum_in_path_path_param_get( @@ -161,7 +171,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("enum_in_path_path_param_get({:?}) - X-Span-ID: {:?}", path_param, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn json_complex_query_param_get( @@ -170,7 +180,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("json_complex_query_param_get({:?}) - X-Span-ID: {:?}", list_of_strings, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn mandatory_request_header_get( @@ -179,7 +189,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("mandatory_request_header_get(\"{}\") - X-Span-ID: {:?}", x_header, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn merge_patch_json_get( @@ -187,7 +197,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("merge_patch_json_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Get some stuff. @@ -196,7 +206,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("multiget_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn multiple_auth_scheme_get( @@ -204,7 +214,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("multiple_auth_scheme_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn one_of_get( @@ -212,7 +222,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("one_of_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn override_server_get( @@ -220,7 +230,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("override_server_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Get some stuff with parameters. @@ -232,7 +242,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("paramget_get({:?}, {:?}, {:?}) - X-Span-ID: {:?}", uuid, some_object, some_list, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn readonly_auth_scheme_get( @@ -240,7 +250,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("readonly_auth_scheme_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn register_callback_post( @@ -249,7 +259,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("register_callback_post(\"{}\") - X-Span-ID: {:?}", url, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn required_octet_stream_put( @@ -258,7 +268,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("required_octet_stream_put({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn responses_with_headers_get( @@ -266,7 +276,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("responses_with_headers_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn rfc7807_get( @@ -274,7 +284,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("rfc7807_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn untyped_property_get( @@ -283,7 +293,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("untyped_property_get({:?}) - X-Span-ID: {:?}", object_untyped_props, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn uuid_get( @@ -291,7 +301,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("uuid_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn xml_extra_post( @@ -300,7 +310,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("xml_extra_post({:?}) - X-Span-ID: {:?}", duplicate_xml_object, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn xml_other_post( @@ -309,7 +319,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("xml_other_post({:?}) - X-Span-ID: {:?}", another_xml_object, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn xml_other_put( @@ -318,7 +328,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("xml_other_put({:?}) - X-Span-ID: {:?}", another_xml_array, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Post an array @@ -328,7 +338,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("xml_post({:?}) - X-Span-ID: {:?}", xml_array, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn xml_put( @@ -337,7 +347,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("xml_put({:?}) - X-Span-ID: {:?}", xml_object, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn create_repo( @@ -346,7 +356,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("create_repo({:?}) - X-Span-ID: {:?}", object_param, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn get_repo_info( @@ -355,7 +365,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_repo_info(\"{}\") - X-Span-ID: {:?}", repo_id, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server_auth.rs new file mode 100644 index 00000000000..7f486733588 --- /dev/null +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server_auth.rs @@ -0,0 +1,129 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use openapi_v3::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + "test.read", + "test.write", + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/auth.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/client/callbacks.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/client/callbacks.rs index cfc2f9059c3..172d284a43c 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/src/client/callbacks.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/client/callbacks.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -52,6 +51,7 @@ mod paths { } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -72,6 +72,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/context.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/context.rs index ac1c07864b8..e01187ca3c8 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/src/context.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; @@ -108,9 +109,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.bearer_authorization(&bearer); let auth_data = AuthData::Bearer(bearer); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/lib.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/lib.rs index e7432c18c65..0830f40a6fa 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/src/lib.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "1.0.7"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] #[must_use] pub enum AnyOfGetResponse { diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/server/mod.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/server/mod.rs index 3825329a51e..0c25b4b8887 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/server/mod.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -50,6 +49,8 @@ use crate::{Api, GetRepoInfoResponse }; +mod server_auth; + pub mod callbacks; mod paths { @@ -122,6 +123,7 @@ mod paths { pub(crate) static ID_XML_OTHER: usize = 23; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -142,6 +144,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -1269,11 +1272,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_object_untyped_props: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_object_untyped_props) => param_object_untyped_props, Err(_) => None, } @@ -1369,11 +1371,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_duplicate_xml_object: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_duplicate_xml_object) => param_duplicate_xml_object, Err(_) => None, } @@ -1437,11 +1438,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_another_xml_object: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_another_xml_object) => param_another_xml_object, Err(_) => None, } @@ -1516,11 +1516,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_another_xml_array: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_another_xml_array) => param_another_xml_array, Err(_) => None, } @@ -1584,11 +1583,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_xml_array: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_xml_array) => param_xml_array, Err(_) => None, } @@ -1652,11 +1650,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_xml_object: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_xml_object) => param_xml_object, Err(_) => None, } @@ -1720,11 +1717,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_object_param: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_object_param) => param_object_param, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/ops-v3/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/ops-v3/.openapi-generator/FILES index e86946909bd..f67b7ce47ca 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/ops-v3/.openapi-generator/FILES @@ -5,14 +5,18 @@ README.md api/openapi.yaml docs/default_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/ops-v3/Cargo.toml b/samples/server/petstore/rust-server/output/ops-v3/Cargo.toml index 75d7eba1f3e..10c46d9c2cd 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/Cargo.toml +++ b/samples/server/petstore/rust-server/output/ops-v3/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -59,6 +59,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/ops-v3/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/ops-v3/examples/client/client_auth.rs new file mode 100644 index 00000000000..3d5200a26be --- /dev/null +++ b/samples/server/petstore/rust-server/output/ops-v3/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use ops_v3::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/ops-v3/examples/client/main.rs b/samples/server/petstore/rust-server/output/ops-v3/examples/client/main.rs index 4b79043b5cc..570a14467c9 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/examples/client/main.rs @@ -4,7 +4,7 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use ops_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use ops_v3::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, Op10GetResponse, Op11GetResponse, Op12GetResponse, @@ -45,6 +45,9 @@ use ops_v3::{Api, ApiNoContext, Client, ContextWrapperExt, models, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -54,6 +57,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -63,43 +70,43 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "Op10Get", - "Op11Get", - "Op12Get", - "Op13Get", - "Op14Get", - "Op15Get", - "Op16Get", - "Op17Get", - "Op18Get", - "Op19Get", - "Op1Get", - "Op20Get", - "Op21Get", - "Op22Get", - "Op23Get", - "Op24Get", - "Op25Get", - "Op26Get", - "Op27Get", - "Op28Get", - "Op29Get", - "Op2Get", - "Op30Get", - "Op31Get", - "Op32Get", - "Op33Get", - "Op34Get", - "Op35Get", - "Op36Get", - "Op37Get", - "Op3Get", - "Op4Get", - "Op5Get", - "Op6Get", - "Op7Get", - "Op8Get", - "Op9Get", + "Op10Get", + "Op11Get", + "Op12Get", + "Op13Get", + "Op14Get", + "Op15Get", + "Op16Get", + "Op17Get", + "Op18Get", + "Op19Get", + "Op1Get", + "Op20Get", + "Op21Get", + "Op22Get", + "Op23Get", + "Op24Get", + "Op25Get", + "Op26Get", + "Op27Get", + "Op28Get", + "Op29Get", + "Op2Get", + "Op30Get", + "Op31Get", + "Op32Get", + "Op33Get", + "Op34Get", + "Op35Get", + "Op36Get", + "Op37Get", + "Op3Get", + "Op4Get", + "Op5Get", + "Op6Get", + "Op7Get", + "Op8Get", + "Op9Get", ]) .required(true) .index(1)) @@ -118,14 +125,40 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/ops-v3/examples/server/main.rs b/samples/server/petstore/rust-server/output/ops-v3/examples/server/main.rs index 2e7d3031162..227fe7e7315 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for ops_v3 implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/ops-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/ops-v3/examples/server/server.rs index cab1bfadc6b..9f97fa0f3b4 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use ops_v3::{ Api, Op10GetResponse, @@ -144,7 +154,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op10_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op11_get( @@ -152,7 +162,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op11_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op12_get( @@ -160,7 +170,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op12_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op13_get( @@ -168,7 +178,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op13_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op14_get( @@ -176,7 +186,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op14_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op15_get( @@ -184,7 +194,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op15_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op16_get( @@ -192,7 +202,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op16_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op17_get( @@ -200,7 +210,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op17_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op18_get( @@ -208,7 +218,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op18_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op19_get( @@ -216,7 +226,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op19_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op1_get( @@ -224,7 +234,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op1_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op20_get( @@ -232,7 +242,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op20_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op21_get( @@ -240,7 +250,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op21_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op22_get( @@ -248,7 +258,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op22_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op23_get( @@ -256,7 +266,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op23_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op24_get( @@ -264,7 +274,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op24_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op25_get( @@ -272,7 +282,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op25_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op26_get( @@ -280,7 +290,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op26_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op27_get( @@ -288,7 +298,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op27_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op28_get( @@ -296,7 +306,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op28_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op29_get( @@ -304,7 +314,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op29_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op2_get( @@ -312,7 +322,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op2_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op30_get( @@ -320,7 +330,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op30_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op31_get( @@ -328,7 +338,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op31_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op32_get( @@ -336,7 +346,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op32_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op33_get( @@ -344,7 +354,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op33_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op34_get( @@ -352,7 +362,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op34_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op35_get( @@ -360,7 +370,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op35_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op36_get( @@ -368,7 +378,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op36_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op37_get( @@ -376,7 +386,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op37_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op3_get( @@ -384,7 +394,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op3_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op4_get( @@ -392,7 +402,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op4_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op5_get( @@ -400,7 +410,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op5_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op6_get( @@ -408,7 +418,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op6_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op7_get( @@ -416,7 +426,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op7_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op8_get( @@ -424,7 +434,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op8_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn op9_get( @@ -432,7 +442,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("op9_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/ops-v3/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/ops-v3/examples/server/server_auth.rs new file mode 100644 index 00000000000..07851419c3e --- /dev/null +++ b/samples/server/petstore/rust-server/output/ops-v3/examples/server/server_auth.rs @@ -0,0 +1,127 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use ops_v3::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/ops-v3/src/auth.rs b/samples/server/petstore/rust-server/output/ops-v3/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/ops-v3/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/ops-v3/src/context.rs b/samples/server/petstore/rust-server/output/ops-v3/src/context.rs index fadd880b965..ee8e118587b 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/src/context.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; diff --git a/samples/server/petstore/rust-server/output/ops-v3/src/lib.rs b/samples/server/petstore/rust-server/output/ops-v3/src/lib.rs index 38647fd39e5..56cf8cabe73 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/src/lib.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "0.0.1"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum Op10GetResponse { /// OK diff --git a/samples/server/petstore/rust-server/output/ops-v3/src/server/mod.rs b/samples/server/petstore/rust-server/output/ops-v3/src/server/mod.rs index d922ae3966f..80e24343691 100644 --- a/samples/server/petstore/rust-server/output/ops-v3/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/ops-v3/src/server/mod.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -61,6 +60,8 @@ use crate::{Api, Op9GetResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -145,6 +146,7 @@ mod paths { pub(crate) static ID_OP9: usize = 36; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -165,6 +167,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static diff --git a/samples/server/petstore/rust-server/output/ops-v3/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/ops-v3/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/ops-v3/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES index c5167aa7470..d72d04fabfa 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES @@ -48,14 +48,18 @@ docs/pet_api.md docs/store_api.md docs/user_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/Cargo.toml b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/Cargo.toml index 4907e0db9aa..f26d4ca2528 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/Cargo.toml +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/Cargo.toml @@ -4,7 +4,7 @@ version = "1.0.0" authors = ["OpenAPI Generator team and contributors"] description = "This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\" license = "Apache-2.0" -edition = "2021" +edition = "2018" publish = ["crates-io"] [features] @@ -71,6 +71,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/client_auth.rs new file mode 100644 index 00000000000..7fc915f98b1 --- /dev/null +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use petstore_with_fake_endpoints_models_for_testing::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/main.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/main.rs index 27bf15f0c0a..b3cd7d27e03 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/client/main.rs @@ -4,7 +4,7 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use petstore_with_fake_endpoints_models_for_testing::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use petstore_with_fake_endpoints_models_for_testing::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, TestSpecialTagsResponse, Call123exampleResponse, FakeOuterBooleanSerializeResponse, @@ -43,6 +43,9 @@ use petstore_with_fake_endpoints_models_for_testing::{Api, ApiNoContext, Client, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -52,6 +55,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -61,31 +68,31 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "Call123example", - "FakeOuterBooleanSerialize", - "FakeOuterCompositeSerialize", - "FakeOuterNumberSerialize", - "FakeOuterStringSerialize", - "FakeResponseWithNumericalDescription", - "HyphenParam", - "TestEndpointParameters", - "TestEnumParameters", - "TestJsonFormData", - "DeletePet", - "FindPetsByStatus", - "FindPetsByTags", - "GetPetById", - "UpdatePetWithForm", - "UploadFile", - "DeleteOrder", - "GetInventory", - "GetOrderById", - "CreateUsersWithArrayInput", - "CreateUsersWithListInput", - "DeleteUser", - "GetUserByName", - "LoginUser", - "LogoutUser", + "Call123example", + "FakeOuterBooleanSerialize", + "FakeOuterCompositeSerialize", + "FakeOuterNumberSerialize", + "FakeOuterStringSerialize", + "FakeResponseWithNumericalDescription", + "HyphenParam", + "TestEndpointParameters", + "TestEnumParameters", + "TestJsonFormData", + "DeletePet", + "FindPetsByStatus", + "FindPetsByTags", + "GetPetById", + "UpdatePetWithForm", + "UploadFile", + "DeleteOrder", + "GetInventory", + "GetOrderById", + "CreateUsersWithArrayInput", + "CreateUsersWithListInput", + "DeleteUser", + "GetUserByName", + "LoginUser", + "LogoutUser", ]) .required(true) .index(1)) @@ -104,14 +111,42 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + "write:pets", + "read:pets", + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/main.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/main.rs index 89c605578a1..63e175c3dc9 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for petstore_with_fake_endpoints_models_for_testing implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs index 5a89df7a6b6..38904f6f568 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use petstore_with_fake_endpoints_models_for_testing::{ Api, TestSpecialTagsResponse, @@ -144,7 +154,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_special_tags({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn call123example( @@ -152,7 +162,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("call123example() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn fake_outer_boolean_serialize( @@ -161,7 +171,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("fake_outer_boolean_serialize({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn fake_outer_composite_serialize( @@ -170,7 +180,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("fake_outer_composite_serialize({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn fake_outer_number_serialize( @@ -179,7 +189,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("fake_outer_number_serialize({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn fake_outer_string_serialize( @@ -188,7 +198,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("fake_outer_string_serialize({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn fake_response_with_numerical_description( @@ -196,7 +206,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("fake_response_with_numerical_description() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn hyphen_param( @@ -205,7 +215,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("hyphen_param(\"{}\") - X-Span-ID: {:?}", hyphen_param, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn test_body_with_query_params( @@ -215,7 +225,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_body_with_query_params(\"{}\", {:?}) - X-Span-ID: {:?}", query, body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// To test \"client\" model @@ -225,7 +235,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_client_model({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트 @@ -248,7 +258,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_endpoint_parameters({}, {}, \"{}\", {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}) - X-Span-ID: {:?}", number, double, pattern_without_delimiter, byte, integer, int32, int64, float, string, binary, date, date_time, password, callback, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// To test enum parameters @@ -264,7 +274,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_enum_parameters({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}) - X-Span-ID: {:?}", enum_header_string_array, enum_header_string, enum_query_string_array, enum_query_string, enum_query_integer, enum_query_double, enum_form_string, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// test inline additionalProperties @@ -274,7 +284,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_inline_additional_properties({:?}) - X-Span-ID: {:?}", param, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// test json serialization of form data @@ -285,7 +295,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_json_form_data(\"{}\", \"{}\") - X-Span-ID: {:?}", param, param2, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// To test class name in snake case @@ -295,7 +305,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("test_classname({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Add a new pet to the store @@ -305,7 +315,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("add_pet({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Deletes a pet @@ -316,7 +326,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("delete_pet({}, {:?}) - X-Span-ID: {:?}", pet_id, api_key, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Finds Pets by status @@ -326,7 +336,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("find_pets_by_status({:?}) - X-Span-ID: {:?}", status, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Finds Pets by tags @@ -336,7 +346,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("find_pets_by_tags({:?}) - X-Span-ID: {:?}", tags, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Find pet by ID @@ -346,7 +356,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_pet_by_id({}) - X-Span-ID: {:?}", pet_id, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Update an existing pet @@ -356,7 +366,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("update_pet({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Updates a pet in the store with form data @@ -368,7 +378,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("update_pet_with_form({}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, name, status, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// uploads an image @@ -380,7 +390,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("upload_file({}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, additional_metadata, file, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Delete purchase order by ID @@ -390,7 +400,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("delete_order(\"{}\") - X-Span-ID: {:?}", order_id, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Returns pet inventories by status @@ -399,7 +409,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_inventory() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Find purchase order by ID @@ -409,7 +419,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_order_by_id({}) - X-Span-ID: {:?}", order_id, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Place an order for a pet @@ -419,7 +429,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("place_order({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Create user @@ -429,7 +439,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("create_user({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Creates list of users with given input array @@ -439,7 +449,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("create_users_with_array_input({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Creates list of users with given input array @@ -449,7 +459,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("create_users_with_list_input({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Delete user @@ -459,7 +469,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("delete_user(\"{}\") - X-Span-ID: {:?}", username, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Get user by user name @@ -469,7 +479,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_user_by_name(\"{}\") - X-Span-ID: {:?}", username, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Logs user into the system @@ -480,7 +490,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("login_user(\"{}\", \"{}\") - X-Span-ID: {:?}", username, password, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Logs out current logged in user session @@ -489,7 +499,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("logout_user() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Updated user @@ -500,7 +510,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("update_user(\"{}\", {:?}) - X-Span-ID: {:?}", username, body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server_auth.rs new file mode 100644 index 00000000000..f5133036d31 --- /dev/null +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server_auth.rs @@ -0,0 +1,129 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use petstore_with_fake_endpoints_models_for_testing::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + "write:pets", + "read:pets", + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/auth.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/context.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/context.rs index b901da55ffc..d821895b62a 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/context.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; @@ -108,9 +109,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.bearer_authorization(&bearer); let auth_data = AuthData::Bearer(bearer); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -119,9 +128,17 @@ impl Service> for AddContext); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -132,9 +149,17 @@ impl Service> for AddContext); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } @@ -143,9 +168,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.basic_authorization(&basic); let auth_data = AuthData::Basic(basic); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs index 45d06cadaae..db00984dad3 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = "/v2"; pub const API_VERSION: &str = "1.0.0"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum TestSpecialTagsResponse { /// successful operation diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs index ff2047c16f4..887af3ab9bf 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs @@ -16,8 +16,7 @@ use multipart::server::Multipart; use multipart::server::save::SaveResult; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -61,6 +60,8 @@ use crate::{Api, UpdateUserResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -155,6 +156,7 @@ mod paths { } } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -175,6 +177,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -268,11 +271,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -378,11 +380,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(_) => None, } @@ -449,11 +450,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(_) => None, } @@ -520,11 +520,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(_) => None, } @@ -591,11 +590,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(_) => None, } @@ -771,11 +769,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -846,11 +843,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -1143,11 +1139,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_param: Option> = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_param) => param_param, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -1262,11 +1257,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -1373,11 +1367,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -1813,11 +1806,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -2329,11 +2321,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -2414,11 +2405,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -2488,11 +2478,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option> = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -2562,11 +2551,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option> = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -2942,11 +2930,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_body: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_body) => param_body, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES index e86946909bd..f67b7ce47ca 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES @@ -5,14 +5,18 @@ README.md api/openapi.yaml docs/default_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/Cargo.toml b/samples/server/petstore/rust-server/output/ping-bearer-auth/Cargo.toml index 97c08a3b740..3a6860128b4 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/Cargo.toml +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -59,6 +59,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/client_auth.rs new file mode 100644 index 00000000000..75571b40f0a --- /dev/null +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use ping_bearer_auth::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/main.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/main.rs index 9fbfcd4259f..3ef61d5f2a4 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/client/main.rs @@ -4,11 +4,14 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use ping_bearer_auth::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use ping_bearer_auth::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, PingGetResponse, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -18,6 +21,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -27,7 +34,7 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "PingGet", + "PingGet", ]) .required(true) .index(1)) @@ -46,14 +53,40 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/main.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/main.rs index 17cee7267f7..6b54873a7e9 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for ping_bearer_auth implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server.rs index 4869efef56a..ffbd60200e8 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use ping_bearer_auth::{ Api, PingGetResponse, @@ -108,7 +118,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("ping_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server_auth.rs new file mode 100644 index 00000000000..e7d21b06931 --- /dev/null +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/examples/server/server_auth.rs @@ -0,0 +1,127 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use ping_bearer_auth::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/auth.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/context.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/context.rs index ac1c07864b8..e01187ca3c8 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/context.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; @@ -108,9 +109,17 @@ impl Service> for AddContext(headers) { + let authorization = self.inner.bearer_authorization(&bearer); let auth_data = AuthData::Bearer(bearer); + let context = context.push(Some(auth_data)); - let context = context.push(None::); + let context = match authorization { + Ok(auth) => context.push(Some(auth)), + Err(err) => { + error!("Error during Authorization: {err:?}"); + context.push(None::) + } + }; return self.inner.call((request, context)) } diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/lib.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/lib.rs index 5377932e173..f7961bcc0d7 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/lib.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "1.0"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum PingGetResponse { /// OK diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/mod.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/mod.rs index 331ede91fd7..7807c3de2be 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/mod.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -25,6 +24,8 @@ use crate::{Api, PingGetResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -37,6 +38,7 @@ mod paths { pub(crate) static ID_PING: usize = 0; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static @@ -57,6 +59,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Has> + Send + Sync + 'static diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +} diff --git a/samples/server/petstore/rust-server/output/rust-server-test/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/rust-server-test/.openapi-generator/FILES index ebf62ae2756..2a4b5711f84 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/rust-server-test/.openapi-generator/FILES @@ -13,14 +13,18 @@ docs/ObjectOfObjects.md docs/ObjectOfObjectsInner.md docs/default_api.md examples/ca.pem +examples/client/client_auth.rs examples/client/main.rs examples/server-chain.pem examples/server-key.pem examples/server/main.rs examples/server/server.rs +examples/server/server_auth.rs +src/auth.rs src/client/mod.rs src/context.rs src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/server/server_auth.rs diff --git a/samples/server/petstore/rust-server/output/rust-server-test/Cargo.toml b/samples/server/petstore/rust-server/output/rust-server-test/Cargo.toml index fb4de1e4651..ef05cb141e3 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/Cargo.toml +++ b/samples/server/petstore/rust-server/output/rust-server-test/Cargo.toml @@ -5,7 +5,7 @@ authors = ["OpenAPI Generator team and contributors"] description = "This spec is for testing rust-server-specific things" # Override this license by providing a License Object in the OpenAPI. license = "Unlicense" -edition = "2021" +edition = "2018" [features] default = ["client", "server"] @@ -59,6 +59,9 @@ frunk_core = { version = "0.3.0", optional = true } frunk-enum-derive = { version = "0.2.0", optional = true } frunk-enum-core = { version = "0.2.0", optional = true } +# Bearer authentication +jsonwebtoken = { version = "9.3.0", optional = false } + [dev-dependencies] clap = "2.25" env_logger = "0.7" diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/client/client_auth.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/client/client_auth.rs new file mode 100644 index 00000000000..be852020274 --- /dev/null +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/client/client_auth.rs @@ -0,0 +1,17 @@ +use rust_server_test::Claims; +use jsonwebtoken::{encode, errors::Error as JwtError, Algorithm, EncodingKey, Header}; +use log::debug; + +/// build an encrypted token with the provided claims. +pub fn build_token(my_claims: Claims, key: &[u8]) -> Result { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = + Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() }; + + let token = encode(&header, &my_claims, &EncodingKey::from_secret(key))?; + debug!("Derived token: {:?}", token); + + Ok(token) +} diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/client/main.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/client/main.rs index e615b83c4f0..d1ae748d59b 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/examples/client/main.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/client/main.rs @@ -4,7 +4,7 @@ #[allow(unused_imports)] use futures::{future, Stream, stream}; #[allow(unused_imports)] -use rust_server_test::{Api, ApiNoContext, Client, ContextWrapperExt, models, +use rust_server_test::{Api, ApiNoContext, Claims, Client, ContextWrapperExt, models, AllOfGetResponse, DummyGetResponse, DummyPutResponse, @@ -17,6 +17,9 @@ use rust_server_test::{Api, ApiNoContext, Client, ContextWrapperExt, models, }; use clap::{App, Arg}; +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + #[allow(unused_imports)] use log::info; @@ -26,6 +29,10 @@ use swagger::{AuthData, ContextBuilder, EmptyContext, Has, Push, XSpanIdString}; type ClientContext = swagger::make_context_ty!(ContextBuilder, EmptyContext, Option, XSpanIdString); +mod client_auth; +use client_auth::build_token; + + // rt may be unused if there are no examples #[allow(unused_mut)] fn main() { @@ -35,13 +42,13 @@ fn main() { .arg(Arg::with_name("operation") .help("Sets the operation to run") .possible_values(&[ - "AllOfGet", - "DummyGet", - "FileResponseGet", - "GetStructuredYaml", - "HtmlPost", - "PostYaml", - "RawJsonGet", + "AllOfGet", + "DummyGet", + "FileResponseGet", + "GetStructuredYaml", + "HtmlPost", + "PostYaml", + "RawJsonGet", ]) .required(true) .index(1)) @@ -60,14 +67,40 @@ fn main() { .help("Port to contact")) .get_matches(); + // Create Bearer-token with a fixed key (secret) for test purposes. + // In a real (production) system this Bearer token should be obtained via an external Identity/Authentication-server + // Ensure that you set the correct algorithm and encodingkey that matches what is used on the server side. + // See https://github.com/Keats/jsonwebtoken for more information + + let auth_token = build_token( + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "my_identity_provider".to_owned(), + // added a very long expiry time + aud: "org.acme.Resource_Server".to_string(), + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + }, + b"secret").unwrap(); + + let auth_data = if !auth_token.is_empty() { + Some(AuthData::Bearer(swagger::auth::Bearer { token: auth_token})) + } else { + // No Bearer-token available, so return None + None + }; + let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", - if is_https { "https" } else { "http" }, - matches.value_of("host").unwrap(), - matches.value_of("port").unwrap()); + if is_https { "https" } else { "http" }, + matches.value_of("host").unwrap(), + matches.value_of("port").unwrap()); let context: ClientContext = - swagger::make_context!(ContextBuilder, EmptyContext, None as Option, XSpanIdString::default()); + swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default()); let mut client : Box> = if matches.is_present("https") { // Using Simple HTTPS diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/main.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/main.rs index 1a3f8022c38..7492252060e 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/main.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/main.rs @@ -1,10 +1,13 @@ //! Main binary entry point for rust_server_test implementation. +// This is the amended version that adds Authorization via Inversion of Control. #![allow(missing_docs)] + use clap::{App, Arg}; mod server; +mod server_auth; /// Create custom server, wire it to the autogenerated router, diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs index f9dcaa3fe26..56fc2f63c4d 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs @@ -30,7 +30,9 @@ pub async fn create(addr: &str, https: bool) { let service = MakeService::new(server); - let service = MakeAllowAllAuthenticator::new(service, "cosmo"); + // This pushes a fourth layer of the middleware-stack even though Swagger assumes only three levels. + // This fourth layer creates an accept-all policy, hower the example-code already acchieves the same via a Bearer-token with full permissions, so next line is not needed (anymore). + // let service = MakeAllowAllAuthenticator::new(service, "cosmo"); #[allow(unused_mut)] let mut service = @@ -56,6 +58,7 @@ pub async fn create(addr: &str, https: bool) { let tls_acceptor = ssl.build(); let tcp_listener = TcpListener::bind(&addr).await.unwrap(); + info!("Starting a server (with https)"); loop { if let Ok((tcp, _)) = tcp_listener.accept().await { let ssl = Ssl::new(tls_acceptor.context()).unwrap(); @@ -75,6 +78,7 @@ pub async fn create(addr: &str, https: bool) { } } } else { + info!("Starting a server (over http, so no TLS)"); // Using HTTP hyper::server::Server::bind(&addr).serve(service).await.unwrap() } @@ -92,6 +96,12 @@ impl Server { } +use jsonwebtoken::{decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use swagger::auth::Authorization; +use crate::server_auth; + + use rust_server_test::{ Api, AllOfGetResponse, @@ -116,7 +126,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("all_of_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// A dummy endpoint to make the spec valid. @@ -125,7 +135,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("dummy_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn dummy_put( @@ -134,7 +144,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("dummy_put({:?}) - X-Span-ID: {:?}", nested_response, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Get a file @@ -143,7 +153,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("file_response_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn get_structured_yaml( @@ -151,7 +161,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("get_structured_yaml() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Test HTML handling @@ -161,7 +171,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("html_post(\"{}\") - X-Span-ID: {:?}", body, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } async fn post_yaml( @@ -170,7 +180,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("post_yaml(\"{}\") - X-Span-ID: {:?}", value, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Get an arbitrary JSON blob. @@ -179,7 +189,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("raw_json_get() - X-Span-ID: {:?}", context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } /// Send an arbitrary JSON blob @@ -189,7 +199,7 @@ impl Api for Server where C: Has + Send + Sync context: &C) -> Result { info!("solo_object_post({:?}) - X-Span-ID: {:?}", value, context.get().0.clone()); - Err(ApiError("Generic failure".into())) + Err(ApiError("Api-Error: Operation is NOT implemented".into())) } } diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server_auth.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server_auth.rs new file mode 100644 index 00000000000..3525e452ac1 --- /dev/null +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server_auth.rs @@ -0,0 +1,127 @@ +use swagger::{ + ApiError, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; +use rust_server_test::{AuthenticationApi, Claims}; +use crate::server::Server; +use jsonwebtoken::{decode, errors as JwtError, decode_header, DecodingKey, TokenData, Validation}; +use swagger::auth::Authorization; +use log::{error, debug}; + +// NOTE: Set environment variable RUST_LOG to the name of the executable (or "cargo run") to activate console logging for all loglevels. +// See https://docs.rs/env_logger/latest/env_logger/ for more details + + +/// Get a dummy claim with full permissions (all scopes) for testing purposes +fn full_permission_claim() -> Claims { + Claims { + sub: "tester@acme.com".to_owned(), + company: "ACME".to_owned(), + iss: "mini-bank-IDP".to_owned(), + aud: "org.acme.Resource_Server".to_string(), + // added a very long expiry time + exp: 10000000000, + // In this example code all available Scopes are added, so the current Bearer Token gets fully authorization. + scopes: [ + ].join(", ") + } +} + + + +/// Extract the data from a Bearer token using the provided Key (secret) and using the HS512-algorithm in this example. +fn extract_token_data(token: &str, key: &[u8]) -> Result, JwtError::Error> { + + // Ensure that you set the correct algorithm and correct key. + // See https://github.com/Keats/jsonwebtoken for more information. + let header = decode_header(token)?; + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["org.acme.Resource_Server"]); + validation.validate_exp = true; + validation + }; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(key), + &validation, + )?; + + Ok(token_data) +} + +/// Build a swagger-Authorization based on the claims (Assuming claims have been extracted from a validated token) +fn build_authorization(claims: Claims) -> Authorization { + let mut scopes = std::collections::BTreeSet::::new(); + claims + .scopes + .split(",") + .map(|s| s.trim()) + .for_each(|s| {let _ = scopes.insert(s.to_string()); }); + let scopes = swagger::auth::Scopes::Some(scopes); + + Authorization{ + subject: claims.sub, + scopes, + issuer: Some(claims.iss)} +} + +fn get_jwt_error_string(error: JwtError::Error) -> String { + match error.kind() { + JwtError::ErrorKind::InvalidSignature => "Incorrect token signature".to_owned(), + JwtError::ErrorKind::InvalidAlgorithm => "The Algorithm is not correct".to_owned(), + JwtError::ErrorKind::ExpiredSignature => "The token has expired".to_owned(), + JwtError::ErrorKind::Base64(e) => format!("Base64 decode failed: {e}"), + JwtError::ErrorKind::Json(e) => format!("JSON decoding: {e}"), + JwtError::ErrorKind::Utf8(e) => format!("Invalid UTF-8: {e}"), + _ => error.to_string() + } +} + + +impl AuthenticationApi for Server where C: Has + Send + Sync { + + /// Implementation of the method to map a Bearer-token to an Authorization + fn bearer_authorization(&self, bearer: &Bearer) -> Result { + debug!("\tAuthorizationApi: Received Bearer-token, {bearer:#?}"); + + match extract_token_data(&bearer.token, b"secret") { + Ok(auth_data) => { + debug!("\tUnpack auth_data as: {auth_data:#?}"); + let authorization = build_authorization(auth_data.claims); + Ok(authorization) + }, + Err(err) => { + let msg = get_jwt_error_string(err); + error!("Failed to unpack Bearer-token: {msg}"); + Err(ApiError(msg)) + } + } + } + + /// Implementation of the method to map an api-key to an Authorization + fn apikey_authorization(&self, api_key: &str) -> Result { + debug!("\tAuthorizationApi: Received api-key, {api_key:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + + /// Implementation of the method to map a basic authentication (username and password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result { + debug!("\tAuthorizationApi: Received Basic-token, {basic:#?}"); + + // TODO: insert the logic to map received apikey to the set of claims + let claims = full_permission_claim(); + + // and build an authorization out of it + Ok(build_authorization(claims)) + } + +} + diff --git a/samples/server/petstore/rust-server/output/rust-server-test/src/auth.rs b/samples/server/petstore/rust-server/output/rust-server-test/src/auth.rs new file mode 100644 index 00000000000..cbaba3dca7c --- /dev/null +++ b/samples/server/petstore/rust-server/output/rust-server-test/src/auth.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeSet; +use crate::server::Authorization; +use serde::{Deserialize, Serialize}; +use swagger::{ApiError, auth::{Basic, Bearer}}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub company: String, + pub exp: u64, + pub scopes: String, +} + + +pub trait AuthenticationApi { + + /// Method should be implemented (see example-code) to map Bearer-token to an Authorization + fn bearer_authorization(&self, token: &Bearer) -> Result; + + /// Method should be implemented (see example-code) to map ApiKey to an Authorization + fn apikey_authorization(&self, token: &str) -> Result; + + /// Method should be implemented (see example-code) to map Basic (Username:password) to an Authorization + fn basic_authorization(&self, basic: &Basic) -> Result; +} + +// Implement it for AllowAllAuthenticator (dummy is needed, but should not used as we have Bearer authorization) +use swagger::auth::{AllowAllAuthenticator, RcBound, Scopes}; + +fn dummy_authorization() -> Authorization { + // Is called when MakeAllowAllAuthenticator is added to the stack. This is not needed as we have Bearer-authorization in the example-code. + // However, if you want to use it anyway this can not be unimplemented, so dummy implementation added. + // unimplemented!() + Authorization{ + subject: "Dummmy".to_owned(), + scopes: Scopes::Some(BTreeSet::new()), // create an empty scope, as this should not be used + issuer: None + } +} + +impl AuthenticationApi for AllowAllAuthenticator +where + RC: RcBound, + RC::Result: Send + 'static { + + /// Get method to map Bearer-token to an Authorization + fn bearer_authorization(&self, _token: &Bearer) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map api-key to an Authorization + fn apikey_authorization(&self, _apikey: &str) -> Result { + Ok(dummy_authorization()) + } + + /// Get method to map basic token to an Authorization + fn basic_authorization(&self, _basic: &Basic) -> Result { + Ok(dummy_authorization()) + } +} diff --git a/samples/server/petstore/rust-server/output/rust-server-test/src/context.rs b/samples/server/petstore/rust-server/output/rust-server-test/src/context.rs index fadd880b965..ee8e118587b 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/src/context.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/src/context.rs @@ -8,7 +8,8 @@ use std::marker::PhantomData; use std::task::{Poll, Context}; use swagger::auth::{AuthData, Authorization, Bearer, Scopes}; use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString}; -use crate::Api; +use crate::{Api, AuthenticationApi}; +use log::error; pub struct MakeAddContext { inner: T, @@ -89,7 +90,7 @@ impl Service> for AddContext, Result=C>, C: Push, Result=D>, D: Send + 'static, - T: Service<(Request, D)> + T: Service<(Request, D)> + AuthenticationApi { type Error = T::Error; type Future = T::Future; diff --git a/samples/server/petstore/rust-server/output/rust-server-test/src/lib.rs b/samples/server/petstore/rust-server/output/rust-server-test/src/lib.rs index 06cbfc39c15..5b86a01acea 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/src/lib.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/src/lib.rs @@ -1,19 +1,26 @@ #![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] #![allow(unused_imports, unused_attributes)] -#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names, clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq, clippy::disallowed_names)] use async_trait::async_trait; use futures::Stream; use std::error::Error; +use std::collections::BTreeSet; use std::task::{Poll, Context}; use swagger::{ApiError, ContextWrapper}; use serde::{Serialize, Deserialize}; +use crate::server::Authorization; + type ServiceError = Box; pub const BASE_PATH: &str = ""; pub const API_VERSION: &str = "2.3.4"; +mod auth; +pub use auth::{AuthenticationApi, Claims}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum AllOfGetResponse { /// OK diff --git a/samples/server/petstore/rust-server/output/rust-server-test/src/server/mod.rs b/samples/server/petstore/rust-server/output/rust-server-test/src/server/mod.rs index 6d0b342010a..69527ec5cf8 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/src/server/mod.rs @@ -14,8 +14,7 @@ use swagger::auth::Scopes; use url::form_urlencoded; #[allow(unused_imports)] -use crate::models; -use crate::header; +use crate::{models, header, AuthenticationApi}; pub use crate::context; @@ -33,6 +32,8 @@ use crate::{Api, SoloObjectPostResponse }; +mod server_auth; + mod paths { use lazy_static::lazy_static; @@ -59,6 +60,7 @@ mod paths { pub(crate) static ID_SOLO_OBJECT: usize = 7; } + pub struct MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -79,6 +81,7 @@ impl MakeService where } } + impl hyper::service::Service for MakeService where T: Api + Clone + Send + 'static, C: Has + Send + Sync + 'static @@ -237,11 +240,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_nested_response: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_nested_response) => param_nested_response, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -548,11 +550,10 @@ impl hyper::service::Service<(Request, C)> for Service where let mut unused_elements = Vec::new(); let param_value: Option = if !body.is_empty() { let deserializer = &mut serde_json::Deserializer::from_slice(&body); - let handle_unknown_field = |path: serde_ignored::Path<'_>| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }; - match serde_ignored::deserialize(deserializer, handle_unknown_field) { + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { Ok(param_value) => param_value, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) diff --git a/samples/server/petstore/rust-server/output/rust-server-test/src/server/server_auth.rs b/samples/server/petstore/rust-server/output/rust-server-test/src/server/server_auth.rs new file mode 100644 index 00000000000..ba78eb2f3f5 --- /dev/null +++ b/samples/server/petstore/rust-server/output/rust-server-test/src/server/server_auth.rs @@ -0,0 +1,28 @@ +use super::Service; +use crate::{Api, AuthenticationApi}; +use swagger::{ + ApiError, + Authorization, + auth::{Basic, Bearer}, + Has, + XSpanIdString}; + +impl AuthenticationApi for Service where +T: Api + Clone + Send + 'static + AuthenticationApi, +C: Has + Has> + Send + Sync + 'static { + + /// Passthrough of the task to the api-implementation + fn bearer_authorization(&self, token: &Bearer) -> Result { + self.api_impl.bearer_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn apikey_authorization(&self, token: &str) -> Result { + self.api_impl.apikey_authorization(token) + } + + /// Passthrough of the task to the api-implementation + fn basic_authorization(&self, basic: &Basic) -> Result { + self.api_impl.basic_authorization(basic) + } +}