- Remove gitlinks (160000 mode) for chainfire, flaredb, iam - Add workspace contents as regular tracked files - Update flake.nix to use simple paths instead of builtins.fetchGit This resolves the nix build failure where submodule directories appeared empty in the nix store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
245 lines
7.5 KiB
Rust
245 lines
7.5 KiB
Rust
//! Authentication provider trait and implementations
|
|
//!
|
|
//! Defines a unified interface for different authentication methods.
|
|
|
|
use async_trait::async_trait;
|
|
use std::sync::Arc;
|
|
|
|
use iam_types::{
|
|
AuthMethod, Error, IamError, InternalTokenClaims, JwtClaims, PrincipalRef, Result,
|
|
};
|
|
|
|
use crate::jwt::JwtVerifier;
|
|
use crate::mtls::{CertificateInfo, MtlsVerifier};
|
|
use crate::token::InternalTokenService;
|
|
|
|
/// Result of authentication
|
|
#[derive(Debug, Clone)]
|
|
pub struct AuthnResult {
|
|
/// Authenticated principal reference
|
|
pub principal_ref: PrincipalRef,
|
|
/// Authentication method used
|
|
pub auth_method: AuthMethod,
|
|
/// Node ID (for service accounts)
|
|
pub node_id: Option<String>,
|
|
/// Organization ID
|
|
pub org_id: Option<String>,
|
|
/// Project ID
|
|
pub project_id: Option<String>,
|
|
/// Groups (from JWT)
|
|
pub groups: Vec<String>,
|
|
/// Original claims (if JWT)
|
|
pub jwt_claims: Option<JwtClaims>,
|
|
/// Internal token claims (if internal token)
|
|
pub internal_claims: Option<InternalTokenClaims>,
|
|
}
|
|
|
|
/// Authentication credentials
|
|
#[derive(Debug, Clone)]
|
|
pub enum AuthnCredentials {
|
|
/// Bearer token (JWT or internal token)
|
|
BearerToken(String),
|
|
/// mTLS certificate info
|
|
Certificate(CertificateInfo),
|
|
/// API key
|
|
ApiKey(String),
|
|
}
|
|
|
|
/// Authentication provider trait
|
|
#[async_trait]
|
|
pub trait AuthnProvider: Send + Sync {
|
|
/// Authenticate using the provided credentials
|
|
async fn authenticate(&self, credentials: &AuthnCredentials) -> Result<AuthnResult>;
|
|
}
|
|
|
|
/// Combined authentication provider supporting multiple methods
|
|
pub struct CombinedAuthProvider {
|
|
jwt_verifier: Option<Arc<JwtVerifier>>,
|
|
mtls_verifier: Option<Arc<MtlsVerifier>>,
|
|
internal_token_service: Option<Arc<InternalTokenService>>,
|
|
}
|
|
|
|
impl CombinedAuthProvider {
|
|
/// Create a new combined auth provider
|
|
pub fn new() -> Self {
|
|
Self {
|
|
jwt_verifier: None,
|
|
mtls_verifier: None,
|
|
internal_token_service: None,
|
|
}
|
|
}
|
|
|
|
/// Add JWT verifier
|
|
pub fn with_jwt(mut self, verifier: JwtVerifier) -> Self {
|
|
self.jwt_verifier = Some(Arc::new(verifier));
|
|
self
|
|
}
|
|
|
|
/// Add mTLS verifier
|
|
pub fn with_mtls(mut self, verifier: MtlsVerifier) -> Self {
|
|
self.mtls_verifier = Some(Arc::new(verifier));
|
|
self
|
|
}
|
|
|
|
/// Add internal token service
|
|
pub fn with_internal_token(mut self, service: InternalTokenService) -> Self {
|
|
self.internal_token_service = Some(Arc::new(service));
|
|
self
|
|
}
|
|
|
|
/// Authenticate a bearer token
|
|
async fn authenticate_bearer(&self, token: &str) -> Result<AuthnResult> {
|
|
// Try internal token first (faster, local verification)
|
|
if let Some(internal_service) = &self.internal_token_service {
|
|
if let Ok(claims) = internal_service.verify(token).await {
|
|
return Ok(AuthnResult {
|
|
principal_ref: PrincipalRef::new(
|
|
claims.principal_kind.clone(),
|
|
&claims.principal_id,
|
|
),
|
|
auth_method: AuthMethod::Internal,
|
|
node_id: claims.node_id.clone(),
|
|
org_id: claims.org_id.clone(),
|
|
project_id: claims.project_id.clone(),
|
|
groups: vec![],
|
|
jwt_claims: None,
|
|
internal_claims: Some(claims),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Try JWT verification
|
|
if let Some(jwt_verifier) = &self.jwt_verifier {
|
|
let claims = jwt_verifier.verify(token).await?;
|
|
|
|
// Map JWT sub to principal
|
|
let principal_ref = PrincipalRef::user(&claims.sub);
|
|
|
|
return Ok(AuthnResult {
|
|
principal_ref,
|
|
auth_method: AuthMethod::Jwt,
|
|
node_id: None,
|
|
org_id: claims.org_id.clone(),
|
|
project_id: claims.project_id.clone(),
|
|
groups: claims.groups.clone(),
|
|
jwt_claims: Some(claims),
|
|
internal_claims: None,
|
|
});
|
|
}
|
|
|
|
Err(Error::Iam(IamError::AuthnFailed(
|
|
"No authentication provider configured for bearer tokens".into(),
|
|
)))
|
|
}
|
|
|
|
/// Authenticate using certificate
|
|
fn authenticate_certificate(&self, cert_info: &CertificateInfo) -> Result<AuthnResult> {
|
|
let mtls_verifier = self.mtls_verifier.as_ref().ok_or_else(|| {
|
|
Error::Iam(IamError::AuthnFailed(
|
|
"mTLS authentication not configured".into(),
|
|
))
|
|
})?;
|
|
|
|
let result = mtls_verifier.verify(cert_info)?;
|
|
|
|
Ok(AuthnResult {
|
|
principal_ref: result.principal_ref,
|
|
auth_method: AuthMethod::Mtls,
|
|
node_id: result.node_id,
|
|
org_id: None,
|
|
project_id: None,
|
|
groups: vec![],
|
|
jwt_claims: None,
|
|
internal_claims: None,
|
|
})
|
|
}
|
|
|
|
/// Authenticate using API key
|
|
async fn authenticate_api_key(&self, _api_key: &str) -> Result<AuthnResult> {
|
|
// API key authentication would typically:
|
|
// 1. Look up the API key in the store
|
|
// 2. Verify it's valid and not expired
|
|
// 3. Return the associated principal
|
|
|
|
// For now, this is a stub
|
|
Err(Error::Iam(IamError::AuthnFailed(
|
|
"API key authentication not yet implemented".into(),
|
|
)))
|
|
}
|
|
}
|
|
|
|
impl Default for CombinedAuthProvider {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl AuthnProvider for CombinedAuthProvider {
|
|
async fn authenticate(&self, credentials: &AuthnCredentials) -> Result<AuthnResult> {
|
|
match credentials {
|
|
AuthnCredentials::BearerToken(token) => self.authenticate_bearer(token).await,
|
|
AuthnCredentials::Certificate(cert_info) => self.authenticate_certificate(cert_info),
|
|
AuthnCredentials::ApiKey(key) => self.authenticate_api_key(key).await,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extract authentication credentials from HTTP headers
|
|
pub fn extract_credentials_from_headers(
|
|
authorization: Option<&str>,
|
|
cert_info: Option<CertificateInfo>,
|
|
) -> Option<AuthnCredentials> {
|
|
// Check for mTLS first (if certificate is provided)
|
|
if let Some(cert) = cert_info {
|
|
return Some(AuthnCredentials::Certificate(cert));
|
|
}
|
|
|
|
// Check Authorization header
|
|
if let Some(auth_header) = authorization {
|
|
if let Some(token) = auth_header.strip_prefix("Bearer ") {
|
|
return Some(AuthnCredentials::BearerToken(token.to_string()));
|
|
}
|
|
if let Some(key) = auth_header.strip_prefix("ApiKey ") {
|
|
return Some(AuthnCredentials::ApiKey(key.to_string()));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_combined_provider_no_config() {
|
|
let provider = CombinedAuthProvider::new();
|
|
|
|
let result = provider
|
|
.authenticate(&AuthnCredentials::BearerToken("some-token".into()))
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_bearer_token() {
|
|
let creds = extract_credentials_from_headers(Some("Bearer abc123"), None);
|
|
|
|
assert!(matches!(
|
|
creds,
|
|
Some(AuthnCredentials::BearerToken(t)) if t == "abc123"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_api_key() {
|
|
let creds = extract_credentials_from_headers(Some("ApiKey secret-key"), None);
|
|
|
|
assert!(matches!(
|
|
creds,
|
|
Some(AuthnCredentials::ApiKey(k)) if k == "secret-key"
|
|
));
|
|
}
|
|
}
|