sunbeam-g2v 0.1.0

Sunbeam Service Framework - A ConnectRPC-based framework for building microservices
Documentation
//! Authentication middleware.

pub mod jwt;
pub mod keto;
#[cfg(feature = "keto")]
pub mod keto_proto;

/// Re-export authentication types.
pub use jwt::{JwtClaims, JwtLayer, JwtValidator, extract_jwt_claims};
pub use keto::{KetoClient, KetoConfig, KetoLayer, Permission, extract_subject};

/// Authentication strategy.
#[derive(Debug, Clone)]
pub enum AuthStrategy {
    /// JWT authentication.
    Jwt(JwtValidator),
    /// Keto authentication.
    Keto(KetoClient),
    /// No authentication.
    None,
}

impl Default for AuthStrategy {
    fn default() -> Self {
        Self::None
    }
}

/// Authentication context.
#[derive(Debug, Clone)]
pub struct AuthContext {
    /// The authenticated subject (user ID).
    pub subject: Option<String>,
    /// JWT claims (if using JWT auth).
    pub claims: Option<jwt::JwtClaims>,
    /// Whether the request is authenticated.
    pub is_authenticated: bool,
}

impl AuthContext {
    /// Create a new unauthenticated context.
    pub fn unauthenticated() -> Self {
        Self {
            subject: None,
            claims: None,
            is_authenticated: false,
        }
    }

    /// Create a new authenticated context.
    pub fn authenticated(subject: impl Into<String>, claims: Option<jwt::JwtClaims>) -> Self {
        Self {
            subject: Some(subject.into()),
            claims,
            is_authenticated: true,
        }
    }

    /// Check if authenticated.
    pub fn is_authenticated(&self) -> bool {
        self.is_authenticated
    }

    /// Get the subject.
    pub fn subject(&self) -> Option<&String> {
        self.subject.as_ref()
    }
}

impl Default for AuthContext {
    fn default() -> Self {
        Self::unauthenticated()
    }
}

/// Authentication middleware layer (stub: no Tower `Layer` impl yet).
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AuthLayer {
    strategy: AuthStrategy,
}

impl AuthLayer {
    /// Create a new authentication layer with the given strategy.
    pub fn new(strategy: AuthStrategy) -> Self {
        Self { strategy }
    }

    /// Create a new JWT authentication layer.
    pub fn jwt(validator: jwt::JwtValidator) -> Self {
        Self::new(AuthStrategy::Jwt(validator))
    }

    /// Create a new Keto authentication layer.
    pub fn keto(client: keto::KetoClient) -> Self {
        Self::new(AuthStrategy::Keto(client))
    }

    /// Create a new authentication layer with no authentication.
    pub fn none() -> Self {
        Self::new(AuthStrategy::None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_auth_context_unauthenticated() {
        let ctx = AuthContext::unauthenticated();
        assert!(!ctx.is_authenticated());
        assert!(ctx.subject().is_none());
    }

    #[test]
    fn test_auth_context_authenticated() {
        let ctx = AuthContext::authenticated("user-1", None);
        assert!(ctx.is_authenticated());
        assert_eq!(ctx.subject(), Some(&"user-1".to_string()));
    }

    #[test]
    fn test_auth_layer_new() {
        let layer = AuthLayer::none();
        match layer.strategy {
            AuthStrategy::None => {}
            _ => panic!("Expected None strategy"),
        }
    }
}