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}
24
25pub(crate) fn same_reusable_identity(
26 registration_key_id: Option<&str>,
27 continuation_key_id: Option<&str>,
28) -> bool {
29 matches!(
30 (registration_key_id, continuation_key_id),
31 (Some(registration), Some(continuation)) if registration == continuation
32 )
33}
34
35pub(crate) fn outcome_key_id_from_snapshot<'a>(
39 caller_key_rec: Option<&'a Arc<futu_auth::KeyRecord>>,
40 authed_key_at_precheck: Option<&'a Arc<futu_auth::KeyRecord>>,
41) -> Option<&'a str> {
42 caller_key_rec
43 .map(|r| r.id.as_str())
44 .or_else(|| authed_key_at_precheck.map(|k| k.id.as_str()))
45}
46
47#[derive(Debug, PartialEq, Eq)]
52pub(crate) enum EarlyTradeScopeDecision {
53 Allow,
55 RejectMissingCallerKey,
57 RejectMissingScope {
59 needed: futu_auth::Scope,
60 key_id: String,
61 },
62}
63
64pub(crate) fn decide_early_trade_scope(
65 env: &str,
66 is_scope_mode: bool,
67 caller_key_rec: Option<&Arc<futu_auth::KeyRecord>>,
68) -> EarlyTradeScopeDecision {
69 if !is_scope_mode {
72 return EarlyTradeScopeDecision::Allow;
73 }
74
75 let is_real = crate::handlers::trade_write::is_real_env(env);
76 let needed_scope = futu_auth::trade_scope_for_env_is_real(is_real);
77
78 let Some(rec) = caller_key_rec else {
79 return EarlyTradeScopeDecision::RejectMissingCallerKey;
80 };
81
82 if !rec.scopes.contains(&needed_scope) {
83 return EarlyTradeScopeDecision::RejectMissingScope {
84 needed: needed_scope,
85 key_id: rec.id.clone(),
86 };
87 }
88
89 EarlyTradeScopeDecision::Allow
90}
91
92pub(super) fn scope_label(s: futu_auth::Scope) -> &'static str {
94 match s {
95 futu_auth::Scope::TradeReal => "trade:real",
96 futu_auth::Scope::TradeSimulate => "trade:simulate",
97 _ => "trade",
98 }
99}
100
101pub(crate) fn http_bearer_token(ctx: &RequestContext<RoleServer>) -> Option<String> {
107 let parts = ctx.extensions.get::<http::request::Parts>()?;
108 let v = parts
109 .headers
110 .get("authorization")
111 .and_then(|v| v.to_str().ok())?;
112 futu_auth_pipeline::parse_bearer_scheme(v).map(|t| t.to_string())
113}
114
115pub(super) fn mcp_audit_context(
122 req_ctx: &RequestContext<RoleServer>,
123) -> futu_auth::audit::AuditContext {
124 let session_id = format!("mcp:{}", req_ctx.id);
125 futu_auth::audit::AuditContext::new(None::<&str>, Some(session_id.as_str()))
126}
127
128pub(super) fn audit_reject_with_context(
129 ctx: &futu_auth::audit::AuditContext,
130 tool: &str,
131 key_id: &str,
132 reason: &str,
133) {
134 futu_auth::audit::with_context(ctx.clone(), || {
135 futu_auth::audit::reject("mcp", tool, key_id, reason);
136 });
137}