mockforge_core/
odata_rewrite.rs1use axum::http::{Request, Uri};
14use std::task::{Context, Poll};
15
16#[derive(Debug, Clone, Copy)]
29pub struct ODataRewriteLayer;
30
31impl<S> tower::Layer<S> for ODataRewriteLayer {
32 type Service = ODataRewriteService<S>;
33
34 fn layer(&self, inner: S) -> Self::Service {
35 ODataRewriteService { inner }
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct ODataRewriteService<S> {
42 inner: S,
43}
44
45impl<S, B> tower::Service<Request<B>> for ODataRewriteService<S>
46where
47 S: tower::Service<Request<B>>,
48{
49 type Response = <S as tower::Service<Request<B>>>::Response;
50 type Error = <S as tower::Service<Request<B>>>::Error;
51 type Future = <S as tower::Service<Request<B>>>::Future;
52
53 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
54 self.inner.poll_ready(cx)
55 }
56
57 fn call(&mut self, mut req: Request<B>) -> Self::Future {
58 let path = req.uri().path();
59
60 if path.contains('(') {
62 let rewritten = rewrite_odata_path(path);
63
64 if rewritten != path {
65 tracing::debug!("OData rewrite: '{}' -> '{}'", path, rewritten);
66
67 let new_uri = if let Some(query) = req.uri().query() {
69 format!("{}?{}", rewritten, query)
70 } else {
71 rewritten
72 };
73
74 if let Ok(uri) = new_uri.parse::<Uri>() {
75 *req.uri_mut() = uri;
76 }
77 }
78 }
79
80 self.inner.call(req)
81 }
82}
83
84pub fn rewrite_odata_path(path: &str) -> String {
89 let mut result = String::with_capacity(path.len());
90 let mut chars = path.chars().peekable();
91
92 while let Some(ch) = chars.next() {
93 if ch == '(' {
94 let mut paren_content = String::new();
96 for c in chars.by_ref() {
97 if c == ')' {
98 break;
99 }
100 paren_content.push(c);
101 }
102
103 if paren_content.is_empty() {
104 continue;
106 }
107
108 if paren_content.contains('=') {
109 for part in paren_content.split(',') {
111 if let Some((_key, value)) = part.split_once('=') {
112 let param = value.trim_matches(|c| c == '\'' || c == '"');
113 result.push('/');
114 result.push_str(param);
115 }
116 }
117 } else {
118 result.push('(');
120 result.push_str(&paren_content);
121 result.push(')');
122 }
123 } else {
124 result.push(ch);
125 }
126 }
127
128 result
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_fast_path_normal_paths() {
137 assert_eq!(rewrite_odata_path("/users"), "/users");
138 assert_eq!(rewrite_odata_path("/users/123"), "/users/123");
139 assert_eq!(rewrite_odata_path("/api/v1/items"), "/api/v1/items");
140 }
141
142 #[test]
143 fn test_single_param_odata_rewrite() {
144 assert_eq!(
145 rewrite_odata_path("/me/getEffectivePermissions(scope='read')"),
146 "/me/getEffectivePermissions/read"
147 );
148 assert_eq!(
149 rewrite_odata_path("/reports/getTeamsUserActivityCounts(period='D7')"),
150 "/reports/getTeamsUserActivityCounts/D7"
151 );
152 }
153
154 #[test]
155 fn test_multi_param_odata_rewrite() {
156 assert_eq!(rewrite_odata_path("/func(key1='val1',key2='val2')"), "/func/val1/val2");
157 }
158
159 #[test]
160 fn test_empty_parens_stripped() {
161 assert_eq!(rewrite_odata_path("/func()"), "/func");
162 assert_eq!(rewrite_odata_path("/a/func()/b"), "/a/func/b");
163 }
164
165 #[test]
166 fn test_nested_odata_in_middle_of_path() {
167 assert_eq!(
168 rewrite_odata_path("/drives/abc/items/xyz/delta(token='foo')"),
169 "/drives/abc/items/xyz/delta/foo"
170 );
171 }
172
173 #[test]
174 fn test_unquoted_values() {
175 assert_eq!(rewrite_odata_path("/func(key=value)"), "/func/value");
176 }
177
178 #[test]
179 fn test_value_without_equals_preserved() {
180 assert_eq!(rewrite_odata_path("/func(something)"), "/func(something)");
182 }
183
184 #[test]
185 fn test_query_string_not_in_path() {
186 assert_eq!(rewrite_odata_path("/func(key='val')"), "/func/val");
189 }
190
191 #[test]
192 fn test_microsoft_graph_odata_paths() {
193 assert_eq!(
195 rewrite_odata_path("/reports/microsoft.graph.getTeamsUserActivityCounts(period='D7')"),
196 "/reports/microsoft.graph.getTeamsUserActivityCounts/D7"
197 );
198 assert_eq!(
199 rewrite_odata_path(
200 "/users/abc/calendar/microsoft.graph.allowedCalendarSharingRoles(User='admin')"
201 ),
202 "/users/abc/calendar/microsoft.graph.allowedCalendarSharingRoles/admin"
203 );
204 }
205
206 #[test]
207 fn test_microsoft_graph_multi_param() {
208 assert_eq!(
209 rewrite_odata_path(
210 "/groups/abc/team/primaryChannel/microsoft.graph.doesUserHaveAccess(userId='u1',tenantId='t1',userPrincipalName='user@example.com')"
211 ),
212 "/groups/abc/team/primaryChannel/microsoft.graph.doesUserHaveAccess/u1/t1/user@example.com"
213 );
214 }
215}