futu_mcp/tool_auth/
policy.rs1use std::collections::HashSet;
2use std::sync::Arc;
3
4use rmcp::{RoleServer, service::RequestContext};
5
6#[derive(Clone)]
15pub(crate) struct CallerSnapshot {
16 pub rec: Option<Arc<futu_auth::KeyRecord>>,
18 pub key_id: Option<String>,
20 pub allowed_acc_ids: Option<HashSet<u64>>,
23 pub bearer_token: Option<String>,
25}
26
27pub(crate) fn outcome_key_id_from_snapshot<'a>(
31 caller_key_rec: Option<&'a Arc<futu_auth::KeyRecord>>,
32 authed_key_at_precheck: Option<&'a Arc<futu_auth::KeyRecord>>,
33) -> Option<&'a str> {
34 caller_key_rec
35 .map(|r| r.id.as_str())
36 .or_else(|| authed_key_at_precheck.map(|k| k.id.as_str()))
37}
38
39#[derive(Debug, PartialEq, Eq)]
44pub(crate) enum EarlyTradeScopeDecision {
45 Allow,
47 RejectMissingCallerKey,
49 RejectMissingScope {
51 needed: futu_auth::Scope,
52 key_id: String,
53 },
54}
55
56pub(crate) fn decide_early_trade_scope(
57 env: &str,
58 is_scope_mode: bool,
59 caller_key_rec: Option<&Arc<futu_auth::KeyRecord>>,
60) -> EarlyTradeScopeDecision {
61 if !is_scope_mode {
64 return EarlyTradeScopeDecision::Allow;
65 }
66
67 let is_real = crate::handlers::trade_write::is_real_env(env);
68 let needed_scope = if is_real {
69 futu_auth::Scope::TradeReal
70 } else {
71 futu_auth::Scope::TradeSimulate
72 };
73
74 let Some(rec) = caller_key_rec else {
75 return EarlyTradeScopeDecision::RejectMissingCallerKey;
76 };
77
78 if !rec.scopes.contains(&needed_scope) {
79 return EarlyTradeScopeDecision::RejectMissingScope {
80 needed: needed_scope,
81 key_id: rec.id.clone(),
82 };
83 }
84
85 EarlyTradeScopeDecision::Allow
86}
87
88pub(super) fn scope_label(s: futu_auth::Scope) -> &'static str {
90 match s {
91 futu_auth::Scope::TradeReal => "trade:real",
92 futu_auth::Scope::TradeSimulate => "trade:simulate",
93 _ => "trade",
94 }
95}
96
97pub(crate) fn http_bearer_token(ctx: &RequestContext<RoleServer>) -> Option<String> {
103 let parts = ctx.extensions.get::<http::request::Parts>()?;
104 let v = parts
105 .headers
106 .get("authorization")
107 .and_then(|v| v.to_str().ok())?;
108 futu_auth_pipeline::parse_bearer_scheme(v).map(|t| t.to_string())
109}
110
111pub(super) fn mcp_audit_context(
118 req_ctx: &RequestContext<RoleServer>,
119) -> futu_auth::audit::AuditContext {
120 let session_id = format!("mcp:{}", req_ctx.id);
121 futu_auth::audit::AuditContext::new(None::<&str>, Some(session_id.as_str()))
122}
123
124pub(super) fn audit_reject_with_context(
125 ctx: &futu_auth::audit::AuditContext,
126 tool: &str,
127 key_id: &str,
128 reason: &str,
129) {
130 futu_auth::audit::with_context(ctx.clone(), || {
131 futu_auth::audit::reject("mcp", tool, key_id, reason);
132 });
133}