wami 0.17.1

Who Am I - Multicloud Identity, IAM, STS, and SSO operations library for Rust
Documentation
//! STS Federation Service
//!
//! Orchestrates federated user token operations.

use crate::store::traits::SessionStore;
use crate::wami::sts::federation::{
    FederatedUser, GetFederationTokenRequest, GetFederationTokenResponse,
};
use crate::wami::sts::session::SessionStatus;
use crate::wami::sts::{Credentials, StsSession};
use chrono::{Duration, Utc};
use std::sync::Arc;
use tokio::sync::RwLock;
use wami_core::arn::{Service, WamiArn};
use wami_core::context::WamiContext;
use wami_core::error::Result;

#[cfg(feature = "sts-jwt")]
use crate::wami::sts::jwt::{self, KeyManager};

/// Service for generating federated user tokens
///
/// Provides high-level operations for federation token creation.
#[wami_macros::service(store_trait = "crate::store::traits::SessionStore")]
pub struct FederationService<S> {
    store: Arc<RwLock<S>>,
}

impl<S: SessionStore> FederationService<S> {
    /// Get a federation token
    ///
    /// Returns temporary credentials for a federated user.
    pub async fn get_federation_token(
        &self,
        context: &WamiContext,
        request: GetFederationTokenRequest,
        principal_arn: &str,
    ) -> Result<GetFederationTokenResponse> {
        self.get_federation_token_inner(context, request, principal_arn)
            .await
    }

    /// Get a federation token with optional JWT signing.
    ///
    /// When a `KeyManager` is provided, the returned credentials will include
    /// a signed JWT (`signed_token`) that can be verified offline.
    #[cfg(feature = "sts-jwt")]
    pub async fn get_federation_token_with_signing(
        &self,
        context: &WamiContext,
        request: GetFederationTokenRequest,
        principal_arn: &str,
        key_manager: Option<&KeyManager>,
    ) -> Result<GetFederationTokenResponse> {
        let mut response = self
            .get_federation_token_inner(context, request, principal_arn)
            .await?;
        if let Some(km) = key_manager {
            let claims_ctx = jwt::StsClaimsContext {
                principal_arn: principal_arn.to_string(),
                issuer: "wami-sts".to_string(),
                audience: "wami".to_string(),
                scoped_actions: vec![],
                scoped_resources: vec![],
            };
            let claims = jwt::build_sts_claims(&response.credentials, &claims_ctx);
            response.credentials.signed_token = km.sign_claims(&claims).ok();
        }
        Ok(response)
    }

    /// Core federation token logic (no JWT signing).
    async fn get_federation_token_inner(
        &self,
        context: &WamiContext,
        request: GetFederationTokenRequest,
        principal_arn: &str,
    ) -> Result<GetFederationTokenResponse> {
        // Validate request
        request.validate()?;

        // Determine session duration (default: 12 hours, max: 36 hours)
        let duration_seconds = request.duration_seconds.unwrap_or(43200);
        let expiration = Utc::now() + Duration::seconds(duration_seconds as i64);

        // Generate credentials
        let access_key_id = format!(
            "AKIA{}",
            uuid::Uuid::new_v4()
                .to_string()
                .replace('-', "")
                .chars()
                .take(16)
                .collect::<String>()
        );
        let secret_access_key = format!(
            "SECRET{}",
            uuid::Uuid::new_v4().to_string().replace('-', "")
        );
        let session_token = format!("TOKEN{}", uuid::Uuid::new_v4().to_string().replace('-', ""));

        let session_arn = format!(
            "arn:aws:sts::{}:federated-user/{}",
            context.instance_id(),
            request.name
        );

        // Build WAMI ARN for credentials using context
        let wami_arn = WamiArn::builder()
            .service(Service::Sts)
            .tenant_path(context.tenant_path().clone())
            .wami_instance(context.instance_id())
            .resource("federated-user", &request.name)
            .build()?;

        let credentials = Credentials {
            access_key_id: access_key_id.clone(),
            secret_access_key: secret_access_key.clone(),
            session_token: session_token.clone(),
            expiration,
            arn: session_arn.clone(),
            wami_arn: wami_arn.clone(),
            providers: vec![],
            tenant_id: None,
            signed_token: None,
        };

        // Create federated user
        let federated_user_id = format!(
            "AIDAI{}",
            uuid::Uuid::new_v4()
                .to_string()
                .replace('-', "")
                .chars()
                .take(13)
                .collect::<String>()
        );
        let federated_user = FederatedUser {
            federated_user_id,
            arn: session_arn.clone(),
        };

        // Create and store session
        let session = StsSession {
            session_token: session_token.clone(),
            access_key_id,
            secret_access_key,
            expiration,
            status: SessionStatus::Active,
            assumed_role_arn: None, // Federation doesn't assume a role
            federated_user_name: Some(request.name.clone()),
            principal_arn: Some(principal_arn.to_string()),
            arn: session_arn,
            wami_arn,
            providers: vec![],
            tenant_id: None,
            created_at: Utc::now(),
            last_used: None,
        };

        self.write_store().await.create_session(session).await?;

        Ok(GetFederationTokenResponse {
            credentials,
            federated_user,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::store::memory::InMemoryWamiStore;

    fn setup_service() -> FederationService<InMemoryWamiStore> {
        let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
        FederationService::new(store)
    }

    fn test_context() -> wami_core::context::WamiContext {
        use wami_core::arn::{TenantPath, WamiArn};
        let arn: WamiArn = "arn:wami:.*:12345678:wami:123456789012:user/test"
            .parse()
            .unwrap();
        wami_core::context::WamiContext::builder()
            .instance_id("123456789012")
            .tenant_path(TenantPath::single(12345678))
            .caller_arn(arn)
            .is_root(false)
            .build()
            .unwrap()
    }

    #[tokio::test]
    async fn test_get_federation_token() {
        let service = setup_service();
        let context = test_context();

        let request = GetFederationTokenRequest {
            name: "federated-user".to_string(),
            duration_seconds: Some(7200),
            policy: Some(r#"{"Version":"2012-10-17","Statement":[]}"#.to_string()),
        };

        let response = service
            .get_federation_token(&context, request, "arn:aws:iam::123456789012:user/alice")
            .await
            .unwrap();

        assert!(!response.credentials.access_key_id.is_empty());
        assert!(!response.credentials.session_token.is_empty());
        assert!(response.federated_user.arn.contains("federated-user"));
    }

    #[tokio::test]
    async fn test_get_federation_token_default_duration() {
        let service = setup_service();

        let request = GetFederationTokenRequest {
            name: "test-federated".to_string(),
            duration_seconds: None, // Should default to 12 hours
            policy: None,
        };

        let context = test_context();
        let response = service
            .get_federation_token(&context, request, "arn:aws:iam::123456789012:user/bob")
            .await
            .unwrap();

        assert!(response.credentials.expiration > Utc::now());
    }

    #[tokio::test]
    async fn test_get_federation_token_invalid_name() {
        let service = setup_service();

        let request = GetFederationTokenRequest {
            name: "invalid name with spaces".to_string(),
            duration_seconds: Some(3600),
            policy: None,
        };

        let context = test_context();
        let result = service
            .get_federation_token(&context, request, "arn:aws:iam::123456789012:user/alice")
            .await;

        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_get_federation_token_creates_session() {
        let service = setup_service();

        let request = GetFederationTokenRequest {
            name: "session-check-user".to_string(),
            duration_seconds: Some(3600),
            policy: None,
        };

        let context = test_context();
        let response = service
            .get_federation_token(&context, request, "arn:aws:iam::123456789012:user/charlie")
            .await
            .unwrap();

        // Verify session was created
        let sessions = service
            .store
            .read()
            .await
            .list_sessions(None)
            .await
            .unwrap();
        assert_eq!(sessions.len(), 1);
        assert_eq!(
            sessions[0].session_token,
            response.credentials.session_token
        );
        assert!(sessions[0].assumed_role_arn.is_none()); // Federation doesn't assume roles
    }

    #[tokio::test]
    async fn test_get_federation_token_with_policy() {
        let service = setup_service();

        let policy = r#"{
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Allow",
                "Action": "s3:GetObject",
                "Resource": "*"
            }]
        }"#;

        let request = GetFederationTokenRequest {
            name: "s3-readonly-user".to_string(),
            duration_seconds: Some(7200),
            policy: Some(policy.to_string()),
        };

        let context = test_context();
        let response = service
            .get_federation_token(&context, request, "arn:aws:iam::123456789012:user/admin")
            .await
            .unwrap();

        assert!(!response.federated_user.federated_user_id.is_empty());
    }

    #[cfg(feature = "sts-jwt")]
    #[tokio::test]
    async fn test_get_federation_token_with_jwt_signing() {
        use crate::wami::sts::jwt::KeyManager;

        let service = setup_service();
        let context = test_context();
        let km = KeyManager::generate();

        let request = GetFederationTokenRequest {
            name: "jwt-fed-user".to_string(),
            duration_seconds: Some(3600),
            policy: None,
        };

        let response = service
            .get_federation_token_with_signing(
                &context,
                request,
                "arn:aws:iam::123456789012:user/alice",
                Some(&km),
            )
            .await
            .unwrap();

        let signed_token = response
            .credentials
            .signed_token
            .as_ref()
            .expect("signed_token should be present");
        let claims = km
            .verify_token(signed_token, "wami")
            .expect("token should be verifiable");
        assert_eq!(claims.sub, "arn:aws:iam::123456789012:user/alice");
        assert_eq!(claims.iss, "wami-sts");
    }
}