//! IAM Token gRPC service implementation use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use sha2::{Digest, Sha256}; use tonic::{Request, Response, Status}; use iam_authn::InternalTokenService; use iam_store::{PrincipalStore, TokenStore}; use iam_types::{ InternalTokenClaims, PrincipalKind as TypesPrincipalKind, PrincipalRef, Scope as TypesScope, TokenMetadata, TokenType, }; use crate::proto::{ self, iam_token_server::IamToken, scope, IssueTokenRequest, IssueTokenResponse, PrincipalKind, RefreshTokenRequest, RefreshTokenResponse, RevokeTokenRequest, RevokeTokenResponse, Scope, ValidateTokenRequest, ValidateTokenResponse, }; /// IAM Token service implementation pub struct IamTokenService { token_service: Arc, principal_store: Arc, token_store: Arc, admin_token: Option, } impl IamTokenService { /// Create a new token service pub fn new( token_service: Arc, principal_store: Arc, token_store: Arc, admin_token: Option, ) -> Self { Self { token_service, principal_store, token_store, admin_token, } } fn convert_scope(proto_scope: &Option) -> TypesScope { match proto_scope { Some(s) => match &s.scope { Some(scope::Scope::System(true)) => TypesScope::System, Some(scope::Scope::Org(org)) => TypesScope::org(&org.id), Some(scope::Scope::Project(proj)) => TypesScope::project(&proj.id, &proj.org_id), Some(scope::Scope::Resource(res)) => { TypesScope::resource(&res.id, &res.project_id, &res.org_id) } _ => TypesScope::System, }, None => TypesScope::System, } } fn convert_scope_to_proto(scope: &TypesScope) -> Scope { Scope { scope: Some(match scope { TypesScope::System => scope::Scope::System(true), TypesScope::Org { id } => scope::Scope::Org(proto::OrgScope { id: id.clone() }), TypesScope::Project { id, org_id } => scope::Scope::Project(proto::ProjectScope { id: id.clone(), org_id: org_id.clone(), }), TypesScope::Resource { id, project_id, org_id, } => scope::Scope::Resource(proto::ResourceScope { id: id.clone(), project_id: project_id.clone(), org_id: org_id.clone(), }), }), } } fn compute_token_id(token: &str) -> String { let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); let digest = hasher.finalize(); URL_SAFE_NO_PAD.encode(digest) } fn now_ts() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } async fn persist_metadata( &self, claims: &InternalTokenClaims, token: &str, ) -> Result { let token_id = Self::compute_token_id(token); let meta = TokenMetadata::new( &token_id, &claims.principal_id, TokenType::Access, claims.iat, claims.exp, ); if let Err(e) = self.token_store.put(&meta).await { return Err(Status::internal(format!( "failed to persist token metadata: {}", e ))); } // ensure we mirror revocation flags if claims already mark them if meta.revoked { return Ok(token_id); } Ok(token_id) } fn admin_token_valid(metadata: &tonic::metadata::MetadataMap, token: &str) -> bool { if let Some(value) = metadata.get("x-iam-admin-token") { if let Ok(raw) = value.to_str() { if raw.trim() == token { return true; } } } if let Some(value) = metadata.get("authorization") { if let Ok(raw) = value.to_str() { let raw = raw.trim(); if let Some(rest) = raw.strip_prefix("Bearer ") { return rest.trim() == token; } if let Some(rest) = raw.strip_prefix("bearer ") { return rest.trim() == token; } } } false } fn require_admin_token(&self, request: &Request) -> Result<(), Status> { if let Some(token) = self.admin_token.as_deref() { if !Self::admin_token_valid(request.metadata(), token) { return Err(Status::unauthenticated( "missing or invalid IAM admin token", )); } } Ok(()) } fn validate_scope_for_principal( principal: &iam_types::Principal, scope: &TypesScope, ) -> Result<(), Status> { let principal_org = principal.org_id.as_deref(); let principal_project = principal.project_id.as_deref(); match principal.kind { TypesPrincipalKind::ServiceAccount => match scope { TypesScope::System => Err(Status::permission_denied( "service accounts cannot mint system-scoped tokens", )), TypesScope::Org { id } => { if principal_org == Some(id.as_str()) { Ok(()) } else { Err(Status::permission_denied( "service account token scope must match principal tenant", )) } } TypesScope::Project { id, org_id } => { if principal_org == Some(org_id.as_str()) && principal_project == Some(id.as_str()) { Ok(()) } else { Err(Status::permission_denied( "service account token scope must match principal tenant", )) } } TypesScope::Resource { project_id, org_id, .. } => { if principal_org == Some(org_id.as_str()) && principal_project == Some(project_id.as_str()) { Ok(()) } else { Err(Status::permission_denied( "service account token scope must match principal tenant", )) } } }, _ => { if let Some(org_id) = principal_org { match scope { TypesScope::System => {} TypesScope::Org { id } if id == org_id => {} TypesScope::Project { org_id: scope_org, .. } | TypesScope::Resource { org_id: scope_org, .. } if scope_org == org_id => {} _ => { return Err(Status::permission_denied( "token scope must match principal tenant", )) } } } Ok(()) } } } } #[tonic::async_trait] impl IamToken for IamTokenService { async fn issue_token( &self, request: Request, ) -> Result, Status> { self.require_admin_token(&request)?; let req = request.into_inner(); // Get principal kind let principal_kind = match PrincipalKind::try_from(req.principal_kind) { Ok(PrincipalKind::User) => TypesPrincipalKind::User, Ok(PrincipalKind::ServiceAccount) => TypesPrincipalKind::ServiceAccount, Ok(PrincipalKind::Group) => TypesPrincipalKind::Group, _ => return Err(Status::invalid_argument("invalid principal kind")), }; // Get principal from store let principal_ref = PrincipalRef::new(principal_kind, &req.principal_id); let principal = self .principal_store .get(&principal_ref) .await .map_err(|e| Status::internal(format!("Failed to get principal: {}", e)))? .ok_or_else(|| Status::not_found("Principal not found"))?; if !principal.enabled { return Err(Status::permission_denied("Principal is disabled")); } // Convert scope let scope = Self::convert_scope(&req.scope); Self::validate_scope_for_principal(&principal, &scope)?; // Determine TTL let ttl = if req.ttl_seconds > 0 { Some(Duration::from_secs(req.ttl_seconds)) } else { None }; // Issue token let issued = self .token_service .issue(&principal, req.roles.clone(), scope, ttl) .await .map_err(|e| Status::internal(format!("Failed to issue token: {}", e)))?; // Persist metadata for revocation tracking self.persist_metadata(&issued.claims, &issued.token).await?; Ok(Response::new(IssueTokenResponse { token: issued.token, expires_at: issued.expires_at, session_id: issued.claims.session_id, })) } async fn validate_token( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); match self.token_service.verify(&req.token).await { Ok(claims) => { // Check revocation list let token_id = Self::compute_token_id(&req.token); if let Some((meta, _)) = self .token_store .get(&claims.principal_id, &token_id) .await .map_err(|e| { Status::internal(format!("Failed to read token metadata: {}", e)) })? { if meta.revoked { return Ok(Response::new(ValidateTokenResponse { valid: false, claims: None, reason: "token revoked".into(), })); } } // Ensure principal still exists and is enabled let principal_ref = PrincipalRef::new(claims.principal_kind.clone(), &claims.principal_id); let principal = self .principal_store .get(&principal_ref) .await .map_err(|e| Status::internal(format!("Failed to read principal: {}", e)))?; let Some(principal) = principal else { return Ok(Response::new(ValidateTokenResponse { valid: false, claims: None, reason: "principal not found".into(), })); }; if !principal.enabled { return Ok(Response::new(ValidateTokenResponse { valid: false, claims: None, reason: "principal disabled".into(), })); } let proto_claims = crate::proto::InternalTokenClaims { principal_id: claims.principal_id.clone(), principal_kind: match claims.principal_kind { TypesPrincipalKind::User => PrincipalKind::User as i32, TypesPrincipalKind::ServiceAccount => PrincipalKind::ServiceAccount as i32, TypesPrincipalKind::Group => PrincipalKind::Group as i32, }, principal_name: claims.principal_name.clone(), roles: claims.roles.clone(), scope: Some(Self::convert_scope_to_proto(&claims.scope)), org_id: claims.org_id.clone(), project_id: claims.project_id.clone(), node_id: claims.node_id.clone(), iat: claims.iat, exp: claims.exp, session_id: claims.session_id.clone(), auth_method: claims.auth_method.to_string(), }; Ok(Response::new(ValidateTokenResponse { valid: true, claims: Some(proto_claims), reason: String::new(), })) } Err(e) => Ok(Response::new(ValidateTokenResponse { valid: false, claims: None, reason: e.to_string(), })), } } async fn revoke_token( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let claims = self .token_service .verify(&req.token) .await .map_err(|e| Status::invalid_argument(format!("Invalid token: {}", e)))?; let token_id = Self::compute_token_id(&req.token); let now = Self::now_ts(); let existing = self .token_store .get(&claims.principal_id, &token_id) .await .map_err(|e| Status::internal(format!("Failed to read token metadata: {}", e)))?; // Try revoking existing metadata; if it does not exist, create a revoked record let revoked = if let Some((meta, _)) = existing { if meta.revoked { true } else { self.token_store .revoke(&claims.principal_id, &token_id, &req.reason, now) .await .map_err(|e| Status::internal(format!("Failed to revoke token: {}", e)))? } } else { let mut meta = TokenMetadata::new( &token_id, &claims.principal_id, TokenType::Access, claims.iat, claims.exp, ); meta.revoke(now, &req.reason); self.token_store .put(&meta) .await .map_err(|e| Status::internal(format!("Failed to write revocation: {}", e)))?; true }; Ok(Response::new(RevokeTokenResponse { success: revoked })) } async fn refresh_token( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); // Validate current token let claims = self .token_service .verify(&req.token) .await .map_err(|e| Status::unauthenticated(format!("Invalid token: {}", e)))?; // Check revocation list let token_id = Self::compute_token_id(&req.token); if let Some((meta, _)) = self .token_store .get(&claims.principal_id, &token_id) .await .map_err(|e| Status::internal(format!("Failed to read token metadata: {}", e)))? { if meta.revoked { return Err(Status::permission_denied("token revoked")); } } // Get principal let principal_kind = claims.principal_kind.clone(); let principal_ref = PrincipalRef::new(principal_kind, &claims.principal_id); let principal = self .principal_store .get(&principal_ref) .await .map_err(|e| Status::internal(format!("Failed to get principal: {}", e)))? .ok_or_else(|| Status::not_found("Principal not found"))?; if !principal.enabled { return Err(Status::permission_denied("Principal is disabled")); } // Determine new TTL let ttl = if req.ttl_seconds > 0 { Some(Duration::from_secs(req.ttl_seconds)) } else { None }; // Issue new token let issued = self .token_service .issue(&principal, claims.roles.clone(), claims.scope.clone(), ttl) .await .map_err(|e| Status::internal(format!("Failed to issue token: {}", e)))?; self.persist_metadata(&issued.claims, &issued.token).await?; Ok(Response::new(RefreshTokenResponse { token: issued.token, expires_at: issued.expires_at, })) } } #[cfg(test)] mod tests { use super::*; use iam_authn::{InternalTokenConfig, SigningKey}; use iam_store::{Backend, TokenStore}; use iam_types::Principal; fn test_setup() -> ( Arc, Arc, Arc, ) { let backend = Arc::new(Backend::memory()); let principal_store = Arc::new(PrincipalStore::new(backend.clone())); let token_store = Arc::new(TokenStore::new(backend)); let signing_key = SigningKey::generate("test-key"); let config = InternalTokenConfig::new(signing_key, "iam-test"); let token_service = Arc::new(InternalTokenService::new(config)); (token_service, principal_store, token_store) } #[tokio::test] async fn test_issue_token_principal_not_found() { let (token_service, principal_store, token_store) = test_setup(); let service = IamTokenService::new(token_service, principal_store, token_store, None); let req = IssueTokenRequest { principal_id: "nonexistent".into(), principal_kind: PrincipalKind::User as i32, roles: vec![], scope: None, ttl_seconds: 3600, }; let result = service.issue_token(Request::new(req)).await; assert!(result.is_err()); } #[tokio::test] async fn test_revoke_and_validate_blocklist() { let (token_service, principal_store, token_store) = test_setup(); let service = IamTokenService::new( token_service, principal_store.clone(), token_store.clone(), None, ); // create principal let principal = Principal::new_user("alice", "Alice"); principal_store.create(&principal).await.unwrap(); // issue token let issue_resp = service .issue_token(Request::new(IssueTokenRequest { principal_id: "alice".into(), principal_kind: PrincipalKind::User as i32, roles: vec!["roles/ProjectAdmin".into()], scope: None, ttl_seconds: 3600, })) .await .unwrap() .into_inner(); // validate OK let valid_resp = service .validate_token(Request::new(ValidateTokenRequest { token: issue_resp.token.clone(), })) .await .unwrap() .into_inner(); assert!(valid_resp.valid); // revoke let revoke_resp = service .revoke_token(Request::new(RevokeTokenRequest { token: issue_resp.token.clone(), reason: "test".into(), })) .await .unwrap() .into_inner(); assert!(revoke_resp.success); // validate should be rejected let invalid_resp = service .validate_token(Request::new(ValidateTokenRequest { token: issue_resp.token.clone(), })) .await .unwrap() .into_inner(); assert!(!invalid_resp.valid); assert_eq!(invalid_resp.reason, "token revoked"); } #[tokio::test] async fn test_validate_token_principal_disabled() { let (token_service, principal_store, token_store) = test_setup(); let service = IamTokenService::new( token_service, principal_store.clone(), token_store.clone(), None, ); let principal = Principal::new_user("alice", "Alice"); principal_store.create(&principal).await.unwrap(); let issue_resp = service .issue_token(Request::new(IssueTokenRequest { principal_id: "alice".into(), principal_kind: PrincipalKind::User as i32, roles: vec!["roles/ProjectAdmin".into()], scope: None, ttl_seconds: 3600, })) .await .unwrap() .into_inner(); let (mut stored, version) = principal_store .get_with_version(&principal.to_ref()) .await .unwrap() .unwrap(); stored.enabled = false; principal_store.update(&stored, version).await.unwrap(); let valid_resp = service .validate_token(Request::new(ValidateTokenRequest { token: issue_resp.token, })) .await .unwrap() .into_inner(); assert!(!valid_resp.valid); assert!(valid_resp.reason.contains("disabled")); } #[tokio::test] async fn test_service_account_system_scope_is_rejected() { let (token_service, principal_store, token_store) = test_setup(); let service = IamTokenService::new( token_service, principal_store.clone(), token_store.clone(), None, ); let principal = Principal::new_service_account("svc-1", "Service 1", "org-1", "proj-1"); principal_store.create(&principal).await.unwrap(); let result = service .issue_token(Request::new(IssueTokenRequest { principal_id: "svc-1".into(), principal_kind: PrincipalKind::ServiceAccount as i32, roles: vec![], scope: Some(IamTokenService::convert_scope_to_proto(&TypesScope::System)), ttl_seconds: 3600, })) .await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code(), tonic::Code::PermissionDenied); } }