vultur_sso_api_client/lib.rs
1//! # VULTUR SSO API Client
2//!
3//! Rust library for integrating with VULTUR SSO in API backends.
4//! Provides JWT validation, authentication middleware, and .well-known endpoint serving.
5//!
6//! ## Features
7//!
8//! - JWT token validation against vultur-ident-api
9//! - Authentication middleware for bouncing unauthenticated requests
10//! - .well-known endpoint serving for permission configurations
11//! - Permission checking utilities
12//! - Axum integration
13//!
14//! ## Basic Example (`Arc<VulturSSOClient>` state)
15//!
16//! ```rust,no_run
17//! use vultur_sso_api_client::{VulturSSOClient, Config, auth_middleware};
18//! use axum::{Router, routing::get, middleware};
19//! use std::sync::Arc;
20//!
21//! #[tokio::main]
22//! async fn main() {
23//! let config = Config::builder()
24//! .ident_api_url("https://ident.vultur.com")
25//! .application_name("my-app")
26//! .build()
27//! .unwrap();
28//!
29//! let client = Arc::new(VulturSSOClient::new(config));
30//!
31//! let app: Router<Arc<VulturSSOClient>> = Router::new()
32//! .route("/protected", get(protected_handler))
33//! .layer(middleware::from_fn_with_state(
34//! client.clone(),
35//! auth_middleware
36//! ))
37//! .with_state(client);
38//! }
39//!
40//! async fn protected_handler() -> &'static str {
41//! "This is protected!"
42//! }
43//! ```
44//!
45//! ## Generic Example (Composite AppState)
46//!
47//! For applications that need composite state (recommended for new projects):
48//!
49//! ```rust,no_run
50//! use vultur_sso_api_client::{VulturSSOClient, Config, auth_middleware_generic};
51//! use axum::{Router, routing::get, middleware, extract::FromRef};
52//! use std::sync::Arc;
53//!
54//! #[derive(Clone)]
55//! struct AppState {
56//! sso_client: Arc<VulturSSOClient>,
57//! database_url: String,
58//! }
59//!
60//! impl FromRef<AppState> for Arc<VulturSSOClient> {
61//! fn from_ref(app_state: &AppState) -> Arc<VulturSSOClient> {
62//! app_state.sso_client.clone()
63//! }
64//! }
65//!
66//! #[tokio::main]
67//! async fn main() {
68//! let config = Config::builder()
69//! .ident_api_url("https://ident.vultur.com")
70//! .application_name("my-app")
71//! .build()
72//! .unwrap();
73//!
74//! let app_state = AppState {
75//! sso_client: Arc::new(VulturSSOClient::new(config)),
76//! database_url: "sqlite://app.db".to_string(),
77//! };
78//!
79//! let app: Router<AppState> = Router::new()
80//! .route("/protected", get(protected_handler))
81//! .layer(middleware::from_fn_with_state(
82//! app_state.clone(),
83//! auth_middleware_generic::<AppState>
84//! ))
85//! .with_state(app_state);
86//! }
87//!
88//! async fn protected_handler() -> &'static str {
89//! "This is protected!"
90//! }
91//! ```
92
93pub mod client;
94pub mod config;
95pub mod error;
96pub mod middleware;
97pub mod permissions;
98pub mod types;
99pub mod well_known;
100
101// Re-export main types and functions
102pub use client::VulturSSOClient;
103pub use config::{Config, ConfigBuilder};
104pub use error::{Result, VulturSSOError};
105pub use middleware::{
106 auth_middleware, auth_middleware_generic, AuthenticatedUser, JwtToken, RequestExt, RequireAuth,
107 RequirePermission,
108};
109pub use permissions::{
110 PermissionChecker, PermissionConfigBuilder, PermissionPatterns, PermissionScopeBuilder,
111};
112pub use types::PermissionScope;
113pub use types::{Permission, PermissionConfig, UserInfo, UserRole};
114pub use well_known::{serve_well_known, WellKnownHandler};
115
116/// Macro to create a permission extractor for a specific permission scope
117///
118/// # Example
119///
120/// ```rust,no_run
121/// use vultur_sso_api_client::{require_permission, VulturSSOClient, Config};
122/// use axum::{extract::State, routing::get, Router};
123/// use std::sync::Arc;
124///
125/// require_permission!(CanManageUsers, "users.admin");
126///
127/// async fn admin_handler(
128/// user: CanManageUsers,
129/// State(client): State<Arc<VulturSSOClient>>,
130/// ) -> String {
131/// format!("Admin access granted to {}", user.user().character_name)
132/// }
133/// ```
134#[macro_export]
135macro_rules! require_permission {
136 ($extractor_name:ident, $permission:expr) => {
137 #[derive(Debug, Clone)]
138 pub struct $extractor_name {
139 user: $crate::UserInfo,
140 }
141
142 impl $extractor_name {
143 pub fn user(&self) -> &$crate::UserInfo {
144 &self.user
145 }
146 }
147
148 impl $crate::middleware::AuthenticatedUser for $extractor_name {
149 fn user(&self) -> &$crate::UserInfo {
150 &self.user
151 }
152 }
153
154 #[axum::async_trait]
155 impl<S> axum::extract::FromRequestParts<S> for $extractor_name
156 where
157 S: Send + Sync,
158 std::sync::Arc<$crate::VulturSSOClient>: axum::extract::FromRef<S>,
159 {
160 type Rejection = $crate::VulturSSOError;
161
162 async fn from_request_parts(
163 parts: &mut axum::http::request::Parts,
164 state: &S,
165 ) -> Result<Self, Self::Rejection> {
166 use $crate::middleware::RequestExt;
167
168 // Get user info from extensions
169 let user = parts
170 .extensions
171 .get::<$crate::UserInfo>()
172 .ok_or_else(|| $crate::VulturSSOError::unauthorized("User not authenticated"))?
173 .clone();
174
175 // Admin users have all permissions
176 if user.is_admin {
177 return Ok($extractor_name { user });
178 }
179
180 // Get JWT token from extensions
181 let token = parts
182 .extensions
183 .get::<$crate::JwtToken>()
184 .ok_or_else(|| $crate::VulturSSOError::unauthorized("JWT token not found"))?;
185
186 // Extract the SSO client from state using FromRef
187 let client: std::sync::Arc<$crate::VulturSSOClient> =
188 axum::extract::FromRef::from_ref(state);
189
190 // Check permission
191 let has_permission = client
192 .check_user_permission(&user.eth_address, $permission, &token.0)
193 .await?;
194
195 if !has_permission {
196 return Err($crate::VulturSSOError::forbidden(format!(
197 "Permission '{}' required",
198 $permission
199 )));
200 }
201
202 Ok($extractor_name { user })
203 }
204 }
205 };
206}