rust_web_server/authz/
layer.rs1use std::sync::Arc;
5
6use super::{Authorizer, Identity, ObjectRef};
7use crate::application::Application;
8use crate::middleware::Middleware;
9use crate::request::Request;
10use crate::response::Response;
11use crate::server::ConnectionInfo;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ObjectTemplate {
20 parts: Vec<Part>,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24enum Part {
25 Literal(String),
26 Path(usize),
27 Query(String),
28 Header(String),
29}
30
31impl ObjectTemplate {
32 pub fn parse(text: &str) -> Result<ObjectTemplate, String> {
33 let mut parts = Vec::new();
34 let mut rest = text;
35 while let Some(open) = rest.find('{') {
36 if open > 0 {
37 parts.push(Part::Literal(rest[..open].to_string()));
38 }
39 let close = rest[open..].find('}').ok_or_else(|| format!("object {text:?}: '{{' without '}}'"))? + open;
40 let inner = &rest[open + 1..close];
41 parts.push(match inner.split_once(':') {
42 Some(("path", n)) => match n.parse::<usize>() {
43 Ok(n) if n >= 1 => Part::Path(n),
44 _ => return Err(format!("object {text:?}: {{path:N}} needs N ≥ 1")),
45 },
46 Some(("query", name)) if !name.is_empty() => Part::Query(name.to_string()),
47 Some(("header", name)) if !name.is_empty() => Part::Header(name.to_string()),
48 _ => return Err(format!("object {text:?}: unknown placeholder {{{inner}}} (use path:N, query:name or header:Name)")),
49 });
50 rest = &rest[close + 1..];
51 }
52 if !rest.is_empty() {
53 parts.push(Part::Literal(rest.to_string()));
54 }
55 if rest.contains('}') || parts.is_empty() {
56 return Err(format!("object {text:?}: stray '}}' or empty"));
57 }
58 let template = ObjectTemplate { parts };
59 if template.object_type().is_none() {
60 return Err(format!("object {text:?}: must start with a literal type, e.g. doc:{{path:2}}"));
61 }
62 Ok(template)
63 }
64
65 pub fn object_type(&self) -> Option<&str> {
67 match self.parts.first() {
68 Some(Part::Literal(text)) => text.split_once(':').map(|(kind, _)| kind).filter(|k| !k.is_empty()),
69 _ => None,
70 }
71 }
72
73 pub fn render(&self, request: &Request) -> Option<ObjectRef> {
82 let raw = crate::url::URL::request_target_path(&request.request_uri).unwrap_or("/");
83 let mut segments = Vec::new();
84 for segment in raw.split('/').filter(|s| !s.is_empty()) {
85 let decoded = crate::app::controller::static_resource::percent_decode_path(segment).ok()?.into_owned();
86 if decoded == "." || decoded == ".." || decoded.contains('/') || decoded.contains('\\') {
87 return None;
88 }
89 segments.push(decoded);
90 }
91 let mut out = String::new();
92 for part in &self.parts {
93 match part {
94 Part::Literal(text) => out.push_str(text),
95 Part::Path(n) => out.push_str(segments.get(n - 1)?),
96 Part::Query(name) => {
97 let query = request.request_uri.split_once('?')?.1;
98 let value = query.split('&').find_map(|pair| pair.split_once('=').filter(|(k, _)| k == name).map(|(_, v)| v))?;
99 if value.is_empty() {
100 return None;
101 }
102 out.push_str(value);
103 }
104 Part::Header(name) => out.push_str(request.header(name).map(|h| h.value.trim()).filter(|v| !v.is_empty())?),
105 }
106 }
107 ObjectRef::parse(&out).ok()
108 }
109}
110
111type Pick = Arc<dyn Fn(&Request) -> Option<(String, ObjectRef)> + Send + Sync>;
112
113#[derive(Clone)]
132pub struct AuthzLayer {
133 authz: Authorizer,
134 pick: Pick,
135}
136
137impl std::fmt::Debug for AuthzLayer {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.debug_struct("AuthzLayer").finish_non_exhaustive()
140 }
141}
142
143impl AuthzLayer {
144 pub fn new(authz: Authorizer, relation: &str, template: &str) -> Result<AuthzLayer, String> {
147 let template = ObjectTemplate::parse(template)?;
148 let kind = template.object_type().unwrap_or_default().to_string();
149 if !authz.schema().has_relation(&kind, relation) {
150 return Err(format!("{kind}#{relation} isn't defined in the schema"));
151 }
152 let relation = relation.to_string();
153 Ok(AuthzLayer { authz, pick: Arc::new(move |request| Some((relation.clone(), template.render(request)?))) })
154 }
155
156 pub fn with(authz: Authorizer, pick: impl Fn(&Request) -> Option<(String, ObjectRef)> + Send + Sync + 'static) -> AuthzLayer {
159 AuthzLayer { authz, pick: Arc::new(pick) }
160 }
161}
162
163impl Middleware for AuthzLayer {
164 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
165 let Some(caller) = Identity::current() else {
166 return Ok(denied(crate::response::STATUS_CODE_REASON_PHRASE.n401_unauthorized, "authentication required"));
167 };
168 let Some((relation, object)) = (self.pick)(request) else {
169 return Ok(denied(crate::response::STATUS_CODE_REASON_PHRASE.n403_forbidden, "forbidden"));
170 };
171 match self.authz.check_subject(&caller.as_subject(), &relation, &object) {
172 Ok(true) => next.execute(request, connection),
173 Ok(false) => Ok(denied(crate::response::STATUS_CODE_REASON_PHRASE.n403_forbidden, "forbidden")),
174 Err(e) => {
175 eprintln!("authz: {} {relation} {object}: {e}; denied", caller.subject());
176 Ok(denied(crate::response::STATUS_CODE_REASON_PHRASE.n403_forbidden, "forbidden"))
177 }
178 }
179 }
180
181 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
182 true
183 }
184}
185
186fn denied(status: &crate::response::StatusCodeReasonPhrase, message: &str) -> Response {
187 Response::json(status, format!(r#"{{"error":"{message}"}}"#).into_bytes())
188}