rust_webx_host/
endpoint.rs1use rust_webx_core::error::Result;
4use rust_webx_core::auth::IAuthorizationPolicy;
5use rust_webx_core::http::IHttpContext;
6use rust_webx_core::routing::IEndpoint;
7use serde_json;
8use std::sync::Arc;
9
10use crate::authz::AuthorizerSet;
11use crate::problem_response::{write_forbidden, write_problem};
12use std::collections::HashMap;
13
14#[allow(clippy::type_complexity)]
16pub struct RequestEndpoint {
17 handler: Box<
18 dyn Fn(
19 &mut dyn IHttpContext,
20 )
21 -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
22 + Send
23 + Sync,
24 >,
25}
26
27#[async_trait::async_trait]
28impl IEndpoint for RequestEndpoint {
29 async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
30 (self.handler)(ctx).await
31 }
32}
33
34impl RequestEndpoint {
35 pub fn new<F>(f: F) -> Self
36 where
37 F: for<'a> Fn(
38 &'a mut dyn IHttpContext,
39 ) -> std::pin::Pin<
40 Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
41 > + Send
42 + Sync
43 + 'static,
44 {
45 Self {
46 handler: Box::new(f),
47 }
48 }
49}
50
51#[allow(clippy::type_complexity)]
58pub struct StubEndpoint {
59 pub method: &'static str,
60 pub path: &'static str,
61 pub handler_type: &'static str,
62 pub dispatch_fn: Option<
63 fn(
64 Vec<u8>,
65 std::collections::HashMap<String, String>,
66 std::collections::HashMap<String, String>,
67 Option<Box<dyn rust_webx_core::auth::IClaims>>,
68 ) -> std::pin::Pin<
69 Box<dyn std::future::Future<Output = Result<rust_webx_core::route::scan::ResponseData>> + Send>,
70 >,
71 >,
72 pub auth_required_role: &'static str,
73 pub auth_required_permission: &'static str,
74 pub resource_policy: Option<Arc<crate::authz::ResourceAuthorization>>,
77 pub authorizers: Option<Arc<AuthorizerSet>>,
81}
82
83#[async_trait::async_trait]
84impl IEndpoint for StubEndpoint {
85 async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
86 if let Some(dispatch) = self.dispatch_fn {
87 let body_bytes = ctx.request().body_bytes().await.unwrap_or_default();
88 let route_params = ctx.request().route_params().clone();
89 let query_params = ctx.request().query().clone();
90
91 let claims = ctx.claims().map(|c| c.clone_box());
94
95 let auth_required = !self.auth_required_role.is_empty()
97 || !self.auth_required_permission.is_empty();
98 if auth_required {
99 match ctx.claims() {
100 None => {
101 write_problem(ctx, 401, "Authentication required").await;
102 return Ok(());
103 }
104 Some(claims) => {
105 if !self.auth_required_role.is_empty()
106 && self.auth_required_role != "authenticated"
107 && !claims
108 .roles()
109 .iter()
110 .any(|r| r.as_str() == self.auth_required_role)
111 {
112 let mut ext = HashMap::new();
113 ext.insert(
114 "required_role".into(),
115 serde_json::Value::String(self.auth_required_role.to_string()),
116 );
117 write_forbidden(
118 ctx,
119 "Forbidden: insufficient permissions",
120 ext,
121 )
122 .await;
123 return Ok(());
124 }
125
126 if !self.auth_required_permission.is_empty()
127 && !claims
128 .permissions()
129 .iter()
130 .any(|p| p.as_str() == self.auth_required_permission)
131 {
132 let mut ext = HashMap::new();
133 ext.insert(
134 "required_permission".into(),
135 serde_json::Value::String(
136 self.auth_required_permission.to_string(),
137 ),
138 );
139 write_forbidden(
140 ctx,
141 "Forbidden: insufficient permissions",
142 ext,
143 )
144 .await;
145 return Ok(());
146 }
147 }
148 }
149 }
150
151 if auth_required {
152 if let Some(ref policy) = self.resource_policy {
153 if let Some(claims) = ctx.claims() {
154 let resource_key = ctx.request().route_pattern().unwrap_or(self.path);
155 if policy.covers_route(resource_key) {
156 let method = ctx.request().method();
157 if let Err(err) =
158 policy.authorize(claims, resource_key, method).await
159 {
160 let mut ext = HashMap::new();
161 ext.insert(
162 "resource".into(),
163 serde_json::Value::String(resource_key.to_string()),
164 );
165 write_forbidden(ctx, &err.to_string(), ext).await;
166 return Ok(());
167 }
168 }
169 }
170 }
171 }
172
173 if auth_required {
175 if let Some(ref authorizers) = self.authorizers {
176 if !authorizers.is_empty() {
177 if let Some(claims) = ctx.claims() {
178 let resource_key = ctx.request().route_pattern().unwrap_or(self.path);
179 let method = ctx.request().method();
180 authorizers.authorize(claims, resource_key, method).await?;
181 }
182 }
183 }
184 }
185
186 let response = dispatch(body_bytes, route_params, query_params, claims).await?;
187
188 ctx.response_mut().set_status(response.status);
189 ctx.response_mut()
190 .set_header("content-type", &response.content_type);
191 ctx.response_mut().write_bytes(response.body).await?;
192 return Ok(());
193 }
194
195 write_problem(
197 ctx,
198 501,
199 &format!(
200 "Route handler not implemented: {} {} (request type: {})",
201 self.method, self.path, self.handler_type
202 ),
203 )
204 .await;
205 Ok(())
206 }
207}
208
209#[allow(clippy::type_complexity)]
211pub struct ControllerEndpoint {
212 handler: Box<
213 dyn Fn(
214 &mut dyn IHttpContext,
215 )
216 -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
217 + Send
218 + Sync,
219 >,
220}
221
222#[async_trait::async_trait]
223impl IEndpoint for ControllerEndpoint {
224 async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
225 (self.handler)(ctx).await
226 }
227}
228
229impl ControllerEndpoint {
230 pub fn new<F>(f: F) -> Self
231 where
232 F: for<'a> Fn(
233 &'a mut dyn IHttpContext,
234 ) -> std::pin::Pin<
235 Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
236 > + Send
237 + Sync
238 + 'static,
239 {
240 Self {
241 handler: Box::new(f),
242 }
243 }
244}
245
246pub struct StaticJsonEndpoint {
252 pub body: Vec<u8>,
253 pub content_type: &'static str,
254}
255
256impl StaticJsonEndpoint {
257 pub fn new(body: Vec<u8>) -> Self {
259 Self {
260 body,
261 content_type: "application/json",
262 }
263 }
264}
265
266#[async_trait::async_trait]
267impl IEndpoint for StaticJsonEndpoint {
268 async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
269 ctx.response_mut().set_status(200);
270 ctx.response_mut()
271 .set_header("content-type", self.content_type);
272 ctx.response_mut().write_bytes(self.body.clone()).await
273 }
274}
275
276pub struct StaticHtmlEndpoint {
280 pub body: &'static str,
281}
282
283#[async_trait::async_trait]
284impl IEndpoint for StaticHtmlEndpoint {
285 async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
286 ctx.response_mut().set_status(200);
287 ctx.response_mut()
288 .set_header("content-type", "text/html; charset=utf-8");
289 ctx.response_mut()
290 .write_bytes(self.body.as_bytes().to_vec())
291 .await
292 }
293}