1use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde_json::Value;
15
16use soma_application::{
17 ExecutionContext, GatewayExecuteRequest, GatewayPort, GatewayPromptRoute, GatewayReloadRequest,
18 GatewayResourceRoute, GatewayRouteScope, GatewayToolRoute, PortError,
19};
20use soma_domain::{
21 actions::{scopes_satisfy, READ_SCOPE},
22 scopes::has_admin_scope,
23 AuthorizationMode,
24};
25use soma_gateway::gateway::dispatch::{
26 dispatch_gateway_action, GatewayAccess, GatewayDispatchError,
27};
28use soma_gateway::gateway::{
29 manager::GatewayManager, manager::GatewayManagerError, protected_routes::ProtectedRouteScope,
30};
31
32pub struct GatewayApplicationPort {
37 gateway: Arc<GatewayManager>,
38}
39
40impl GatewayApplicationPort {
41 pub fn new(gateway: Arc<GatewayManager>) -> Self {
42 Self { gateway }
43 }
44
45 async fn dispatch(
46 &self,
47 action: &str,
48 params: Value,
49 context: &ExecutionContext,
50 ) -> Result<Value, PortError> {
51 dispatch_gateway_action(&self.gateway, gateway_access(context), action, params)
52 .await
53 .map_err(|error| gateway_port_error(action, error))
54 }
55}
56
57#[async_trait]
58impl GatewayPort for GatewayApplicationPort {
59 async fn status(&self, context: &ExecutionContext) -> Result<Value, PortError> {
60 self.dispatch("gateway.list", serde_json::json!({}), context)
61 .await
62 }
63
64 async fn reload(
65 &self,
66 request: GatewayReloadRequest,
67 context: &ExecutionContext,
68 ) -> Result<Value, PortError> {
69 self.dispatch("gateway.reload", request.config, context)
70 .await
71 }
72
73 async fn execute(
74 &self,
75 request: GatewayExecuteRequest,
76 context: &ExecutionContext,
77 ) -> Result<Value, PortError> {
78 self.dispatch(&request.action, request.params, context)
79 .await
80 }
81
82 async fn list_mcp_tools(
83 &self,
84 scope: Option<&GatewayRouteScope>,
85 context: &ExecutionContext,
86 ) -> Result<Vec<GatewayToolRoute>, PortError> {
87 let scope = scope.map(protected_route_scope);
88 self.gateway
89 .tool_routes_for_subject_and_scope(Some(gateway_subject(context)), scope.as_ref())
90 .await
91 .map(|routes| {
92 routes
93 .into_iter()
94 .map(|route| GatewayToolRoute {
95 name: route.name,
96 description: route.descriptor.description,
97 input_schema: route.descriptor.input_schema,
98 output_schema: route.descriptor.output_schema,
99 destructive: route.descriptor.destructive,
100 })
101 .collect()
102 })
103 .map_err(|error| gateway_manager_port_error("tools/list", error))
104 }
105
106 async fn call_mcp_tool(
107 &self,
108 name: &str,
109 params: Value,
110 scope: Option<&GatewayRouteScope>,
111 context: &ExecutionContext,
112 ) -> Result<Option<Value>, PortError> {
113 let scope = scope.map(protected_route_scope);
114 self.gateway
115 .call_mcp_tool_for_subject_and_scope(
116 name,
117 params,
118 Some(gateway_subject(context)),
119 scope.as_ref(),
120 )
121 .await
122 .map_err(|error| gateway_manager_port_error("tools/call", error))
123 }
124
125 async fn list_mcp_resources(
126 &self,
127 scope: Option<&GatewayRouteScope>,
128 context: &ExecutionContext,
129 ) -> Result<Vec<GatewayResourceRoute>, PortError> {
130 let scope = scope.map(protected_route_scope);
131 self.gateway
132 .resource_routes_for_subject_and_scope(Some(gateway_subject(context)), scope.as_ref())
133 .await
134 .map(|routes| {
135 routes
136 .into_iter()
137 .map(|route| GatewayResourceRoute {
138 uri: route.uri,
139 native_uri: route.native_uri,
140 name: route.descriptor.name,
141 })
142 .collect()
143 })
144 .map_err(|error| gateway_manager_port_error("resources/list", error))
145 }
146
147 async fn read_mcp_resource(
148 &self,
149 uri: &str,
150 scope: Option<&GatewayRouteScope>,
151 context: &ExecutionContext,
152 ) -> Result<Option<Value>, PortError> {
153 let scope = scope.map(protected_route_scope);
154 self.gateway
155 .read_mcp_resource_for_subject_and_scope(
156 uri,
157 Some(gateway_subject(context)),
158 scope.as_ref(),
159 )
160 .await
161 .map_err(|error| gateway_manager_port_error("resources/read", error))
162 }
163
164 async fn list_mcp_prompts(
165 &self,
166 scope: Option<&GatewayRouteScope>,
167 context: &ExecutionContext,
168 ) -> Result<Vec<GatewayPromptRoute>, PortError> {
169 let scope = scope.map(protected_route_scope);
170 self.gateway
171 .prompt_routes_for_subject_and_scope(Some(gateway_subject(context)), scope.as_ref())
172 .await
173 .map(|routes| {
174 routes
175 .into_iter()
176 .map(|route| GatewayPromptRoute {
177 name: route.name,
178 description: route.descriptor.description,
179 })
180 .collect()
181 })
182 .map_err(|error| gateway_manager_port_error("prompts/list", error))
183 }
184
185 async fn get_mcp_prompt(
186 &self,
187 name: &str,
188 arguments: Option<serde_json::Map<String, Value>>,
189 scope: Option<&GatewayRouteScope>,
190 context: &ExecutionContext,
191 ) -> Result<Option<Value>, PortError> {
192 let scope = scope.map(protected_route_scope);
193 self.gateway
194 .get_mcp_prompt_for_subject_and_scope(
195 name,
196 arguments,
197 Some(gateway_subject(context)),
198 scope.as_ref(),
199 )
200 .await
201 .map_err(|error| gateway_manager_port_error("prompts/get", error))
202 }
203}
204
205fn gateway_subject(context: &ExecutionContext) -> &str {
206 const SHARED_GATEWAY_SUBJECT: &str = "gateway";
207 if !matches!(context.authorization_mode, AuthorizationMode::Mounted) {
208 return SHARED_GATEWAY_SUBJECT;
209 }
210 let Some(principal) = context.principal.as_ref() else {
211 return SHARED_GATEWAY_SUBJECT;
212 };
213 if principal.issuer.as_deref() == Some("local") || has_admin_scope(&principal.scopes.to_vec()) {
214 SHARED_GATEWAY_SUBJECT
215 } else {
216 &principal.subject
217 }
218}
219
220fn protected_route_scope(scope: &GatewayRouteScope) -> ProtectedRouteScope {
221 ProtectedRouteScope {
222 upstreams: scope.upstreams.clone(),
223 services: scope.services.clone(),
224 expose_code_mode: scope.expose_code_mode,
225 }
226}
227
228fn gateway_access(context: &ExecutionContext) -> GatewayAccess {
229 if !matches!(context.authorization_mode, AuthorizationMode::Mounted) {
230 return GatewayAccess {
231 read: true,
232 admin: true,
233 };
234 }
235 let scopes = context
236 .principal
237 .as_ref()
238 .map(|principal| principal.scopes.to_vec())
239 .unwrap_or_default();
240 let admin = has_admin_scope(&scopes);
241 GatewayAccess {
242 read: admin || scopes_satisfy(&scopes, READ_SCOPE),
243 admin,
244 }
245}
246
247fn gateway_port_error(action: &str, error: GatewayDispatchError) -> PortError {
248 let message = error.to_string();
249 port_error_from_structured(error.structured(action), message)
250}
251
252fn gateway_manager_port_error(operation: &str, error: GatewayManagerError) -> PortError {
261 let message = format!("{operation} failed: {error}");
262 port_error_from_structured(
263 GatewayDispatchError::from(error).structured(operation),
264 message,
265 )
266}
267
268fn port_error_from_structured(
269 structured: soma_gateway::dispatch_helpers::GatewayStructuredError,
270 message: String,
271) -> PortError {
272 PortError {
273 code: structured.code.to_owned(),
274 message,
275 retryable: matches!(structured.kind, "runtime"),
276 remediation: structured.remediation.to_owned(),
277 }
278}
279
280#[cfg(test)]
281#[path = "gateway_tests.rs"]
282mod tests;