From eb329c62eb6aaa9e6a1e15cd8265c4fc11ec6b82 Mon Sep 17 00:00:00 2001 From: Raezil Date: Sat, 27 Dec 2025 19:50:14 +0100 Subject: [PATCH] Add 0.9.1 changelog entries --- CHANGELOG.md | 6 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- docs/changelog.md | 12 ++++ src/openapi.rs | 156 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 docs/changelog.md diff --git a/CHANGELOG.md b/CHANGELOG.md index df57509..1700fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.1] - 2025-12-27 + +### Added +- OpenAPI security scheme parsing and RestConnector authentication support. +- `with_auth` builder method for OpenAPI parser to configure API key and bearer token credentials. +- Updated documentation and tests for security configuration. ## [0.9.0] - 2025-12-27 diff --git a/Cargo.lock b/Cargo.lock index a8e54de..9733925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "grpc_graphql_gateway" -version = "0.9.0" +version = "0.9.1" dependencies = [ "ahash", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index b4772ec..5583a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grpc_graphql_gateway" -version = "0.9.0" +version = "0.9.1" edition = "2021" authors = ["Protocol Lattice"] description = "A Rust implementation of gRPC-GraphQL gateway - generates GraphQL execution code from gRPC services" diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..97c692e --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. + +## [0.9.1] - 2025-12-27 + +### Added +- OpenAPI security scheme parsing and RestConnector authentication support. +- `with_auth` builder method for OpenAPI parser to configure API key and bearer token credentials. +- Updated documentation and tests for security configuration. diff --git a/src/openapi.rs b/src/openapi.rs index 8bb6581..f6ccfc2 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -36,13 +36,15 @@ //! ``` use crate::rest_connector::{ - HttpMethod, RestConnector, RestEndpoint, RestFieldType, RestResponseField, RestResponseSchema, + ApiKeyInterceptor, BearerAuthInterceptor, HttpMethod, RestConnector, RestEndpoint, + RestFieldType, RestResponseField, RestResponseSchema, }; use crate::{Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use std::collections::HashMap; use std::path::Path; +use std::sync::Arc; use std::time::Duration; /// OpenAPI specification parser @@ -56,6 +58,7 @@ pub struct OpenApiParser { operation_filter: Option bool + Send + Sync>>, tag_filter: Option>, prefix: Option, + auth_config: HashMap, } impl std::fmt::Debug for OpenApiParser { @@ -94,9 +97,16 @@ pub struct OpenApiSpec { /// Component schemas #[serde(default)] pub components: Option, + /// Global security requirements + #[serde(default)] + pub security: Vec>>, /// Swagger 2.0 definitions (equivalent to components.schemas) #[serde(default)] pub definitions: Option>, + /// Swagger 2.0 security definitions + #[serde(default)] + #[serde(rename = "securityDefinitions")] + pub security_definitions: Option>, /// Swagger 2.0 host #[serde(default)] pub host: Option, @@ -179,6 +189,9 @@ pub struct Operation { /// Response definitions #[serde(default)] pub responses: HashMap, + /// Security requirements override + #[serde(default)] + pub security: Option>>>, /// Whether the operation is deprecated #[serde(default)] pub deprecated: bool, @@ -248,6 +261,10 @@ pub struct Components { /// Schema definitions #[serde(default)] pub schemas: HashMap, + /// Security schemes + #[serde(default)] + #[serde(rename = "securitySchemes")] + pub security_schemes: HashMap, } /// Schema object definition @@ -291,6 +308,31 @@ pub struct SchemaObject { pub any_of: Option>, } +/// Security scheme definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecurityScheme { + /// Scheme type (apiKey, http, oauth2, openIdConnect) + #[serde(rename = "type")] + pub scheme_type: String, + /// Scheme description + #[serde(default)] + pub description: Option, + /// Header/query parameter name (for apiKey) + #[serde(default)] + pub name: Option, + /// Location (query, header, cookie) (for apiKey) + #[serde(default)] + #[serde(rename = "in")] + pub location: Option, + /// HTTP scheme (bearer, basic) (for http) + #[serde(default)] + pub scheme: Option, + /// Bearer format hint (for http/bearer) + #[serde(default)] + #[serde(rename = "bearerFormat")] + pub bearer_format: Option, +} + impl OpenApiParser { /// Create a parser from a file path /// @@ -337,6 +379,7 @@ impl OpenApiParser { operation_filter: None, tag_filter: None, prefix: None, + auth_config: HashMap::new(), }) } @@ -352,6 +395,7 @@ impl OpenApiParser { operation_filter: None, tag_filter: None, prefix: None, + auth_config: HashMap::new(), }) } @@ -413,6 +457,19 @@ impl OpenApiParser { self } + /// Set credentials for a security scheme + /// + /// The `scheme_name` must match the security scheme name in the OpenAPI spec. + /// + /// # Info + /// - For `apiKey` (header): The value is the key itself + /// - For `http` (bearer): The value is the token (without "Bearer " prefix) + /// - For `http` (basic): The value is the encoded string or unused (not yet fully supported) + pub fn with_auth(mut self, scheme_name: impl Into, value: impl Into) -> Self { + self.auth_config.insert(scheme_name.into(), value.into()); + self + } + /// Get the parsed OpenAPI spec pub fn spec(&self) -> &OpenApiSpec { &self.spec @@ -430,6 +487,38 @@ impl OpenApiParser { .base_url(&base_url) .timeout(self.timeout); + // Configure authentication + let security_schemes = self.get_security_schemes(); + for (name, scheme) in &security_schemes { + if let Some(value) = self.auth_config.get(name) { + match scheme.scheme_type.as_str() { + "apiKey" => { + if let (Some(ref param_name), Some(ref location)) = + (&scheme.name, &scheme.location) + { + if location == "header" { + builder = builder.interceptor(Arc::new(ApiKeyInterceptor::new( + param_name.clone(), + value.clone(), + ))); + } + // TODO: Support query param auth if needed + } + } + "http" => { + if let Some(ref auth_scheme) = scheme.scheme { + if auth_scheme.eq_ignore_ascii_case("bearer") { + builder = builder.interceptor(Arc::new( + BearerAuthInterceptor::new(value.clone()), + )); + } + } + } + _ => {} + } + } + } + // Get schemas for reference resolution let schemas = self.get_schemas(); @@ -524,6 +613,23 @@ impl OpenApiParser { schemas } + /// Get all security schemes (from components or definitions) + fn get_security_schemes(&self) -> HashMap { + let mut schemes = HashMap::new(); + + // OpenAPI 3.0+ components.securitySchemes + if let Some(ref components) = self.spec.components { + schemes.extend(components.security_schemes.clone()); + } + + // Swagger 2.0 securityDefinitions + if let Some(ref definitions) = self.spec.security_definitions { + schemes.extend(definitions.clone()); + } + + schemes + } + /// Create an endpoint from an operation fn create_endpoint( &self, @@ -1061,4 +1167,52 @@ mod tests { let pet_schema = schemas.get("Pet").unwrap(); assert_eq!(pet_schema.schema_type.as_deref(), Some("object")); } + + #[test] + fn test_security_scheme_parsing_and_config() { + let json = r##"{ + "openapi": "3.0.0", + "info": { + "title": "Secure API", + "version": "1.0.0" + }, + "servers": [ + {"url": "https://api.secure.com"} + ], + "paths": { + "/secure": { + "get": { + "operationId": "getSecure", + "responses": { + "200": {"description": "OK"} + } + } + } + }, + "components": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer" + } + } + } + }"##; + + let parser = OpenApiParser::from_string(json, false) + .unwrap() + .with_auth("apiKeyAuth", "secret-key") + .with_auth("bearerAuth", "some-token"); + + let connector = parser.build().unwrap(); + + assert_eq!(connector.base_url(), "https://api.secure.com"); + } + + }