use std::sync::Arc; use axum::http::{HeaderMap, StatusCode, header::AUTHORIZATION}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use crank_core::{OperationSecurityLevel, PlatformApiKeyScope}; use sha2::{Digest, Sha256}; use time::OffsetDateTime; use crate::{ app::{AgentRoutePath, AppState}, auth::VerifiedMachineCredential, }; pub(super) async fn require_machine_access( state: &Arc, path: &AgentRoutePath, headers: &HeaderMap, required_scope: PlatformApiKeyScope, ) -> Result { let secret = bearer_token(headers).ok_or(StatusCode::UNAUTHORIZED)?; let credential = resolve_machine_credential(state, path, secret).await?; if !allows_scope(&credential.scopes, required_scope) { return Err(StatusCode::FORBIDDEN); } Ok(credential) } pub(super) async fn require_approval_access( state: &Arc, path: &AgentRoutePath, headers: &HeaderMap, required_scope: PlatformApiKeyScope, ) -> Result { let secret = bearer_token(headers).ok_or(StatusCode::UNAUTHORIZED)?; let secret_hash = hash_access_secret(secret); let Some(api_key) = state .registry .get_approval_api_key_by_secret_for_agent_slug( &path.workspace_slug, &path.agent_slug, &secret_hash, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? else { return Err(StatusCode::UNAUTHORIZED); }; if !approval_allows_scope(&api_key.api_key.scopes, required_scope) { return Err(StatusCode::FORBIDDEN); } let used_at = OffsetDateTime::now_utc(); state .registry .touch_platform_api_key(&api_key.api_key.workspace_id, &api_key.api_key.id, &used_at) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(api_key) } pub(super) fn bearer_token(headers: &HeaderMap) -> Option<&str> { let value = headers.get(AUTHORIZATION)?.to_str().ok()?; let (scheme, token) = value.split_once(' ')?; if !scheme.eq_ignore_ascii_case("Bearer") || token.is_empty() { return None; } Some(token) } pub(super) fn credential_allows_security_level( credential: &VerifiedMachineCredential, required_level: OperationSecurityLevel, ) -> bool { security_level_rank(credential.max_security_level) >= security_level_rank(required_level) } pub(super) fn serialize_security_level(level: OperationSecurityLevel) -> &'static str { match level { OperationSecurityLevel::Standard => "standard", } } pub(super) fn serialize_machine_access_mode(mode: crank_core::MachineAccessMode) -> &'static str { match mode { crank_core::MachineAccessMode::StaticAgentKey => "static_agent_key", } } pub(super) fn hash_access_secret(secret: &str) -> String { let digest = Sha256::digest(secret.as_bytes()); URL_SAFE_NO_PAD.encode(digest) } async fn resolve_machine_credential( state: &Arc, path: &AgentRoutePath, token: &str, ) -> Result { if let Some(credential) = verify_static_agent_key(state, path, token).await? { return Ok(credential); } state .credential_verifier .verify_bearer_token(&path.workspace_slug, &path.agent_slug, token) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::UNAUTHORIZED) } async fn verify_static_agent_key( state: &Arc, path: &AgentRoutePath, secret: &str, ) -> Result, StatusCode> { let secret_hash = hash_access_secret(secret); let Some(api_key) = state .registry .get_platform_api_key_by_secret_for_agent_slug( &path.workspace_slug, &path.agent_slug, &secret_hash, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? else { return Ok(None); }; let used_at = OffsetDateTime::now_utc(); state .registry .touch_platform_api_key(&api_key.api_key.workspace_id, &api_key.api_key.id, &used_at) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Some(VerifiedMachineCredential { machine_access_mode: crank_core::MachineAccessMode::StaticAgentKey, max_security_level: crank_core::OperationSecurityLevel::Standard, scopes: api_key.api_key.scopes, })) } fn allows_scope(scopes: &[PlatformApiKeyScope], required_scope: PlatformApiKeyScope) -> bool { match required_scope { PlatformApiKeyScope::Read => scopes.iter().any(|scope| { matches!( scope, PlatformApiKeyScope::Read | PlatformApiKeyScope::Write | PlatformApiKeyScope::Deploy ) }), PlatformApiKeyScope::Write => scopes.iter().any(|scope| { matches!( scope, PlatformApiKeyScope::Write | PlatformApiKeyScope::Deploy ) }), PlatformApiKeyScope::Deploy => scopes .iter() .any(|scope| matches!(scope, PlatformApiKeyScope::Deploy)), PlatformApiKeyScope::Approve | PlatformApiKeyScope::Deny | PlatformApiKeyScope::ReadPending => false, } } fn approval_allows_scope( scopes: &[PlatformApiKeyScope], required_scope: PlatformApiKeyScope, ) -> bool { scopes.iter().any(|scope| match required_scope { PlatformApiKeyScope::Approve => matches!(scope, PlatformApiKeyScope::Approve), PlatformApiKeyScope::Deny => matches!(scope, PlatformApiKeyScope::Deny), PlatformApiKeyScope::ReadPending => matches!(scope, PlatformApiKeyScope::ReadPending), PlatformApiKeyScope::Read | PlatformApiKeyScope::Write | PlatformApiKeyScope::Deploy => { false } }) } fn security_level_rank(level: OperationSecurityLevel) -> u8 { match level { OperationSecurityLevel::Standard => 0, } }