assay/lua/policy/
apply.rs1use std::sync::Arc;
7
8use mlua::{Lua, MultiValue, Table, Value};
9
10use super::{
11 active, credential, guard_http, is_redacted_header, redact_json_text, redact_keys,
12 response_limit,
13};
14
15const VERBS: &[&str] = &["get", "post", "put", "patch", "delete"];
16
17type Target = Arc<dyn Fn(&MultiValue) -> Option<(String, String, usize)>>;
20
21pub fn apply(lua: &Lua) -> mlua::Result<()> {
22 guard_os_getenv(lua)?;
23 let Some(http) = lua.globals().get::<Option<Table>>("http")? else {
24 return Ok(());
25 };
26 for verb in VERBS {
27 wrap(lua, &http, verb, verb_target(verb))?;
28 }
29 wrap(lua, &http, "_client_request", client_request_target())?;
32 wrap(lua, &http, "download", verb_target("get"))?;
33 Ok(())
34}
35
36fn guard_os_getenv(lua: &Lua) -> mlua::Result<()> {
43 for os_table in crate::lua::builtins::gated::tables_for(lua, "os")? {
44 let Some(inner) = os_table.get::<Option<mlua::Function>>("getenv")? else {
45 continue;
46 };
47 let wrapper = lua.create_function(move |lua, args: mlua::MultiValue| {
48 let name = match args.iter().next() {
49 Some(Value::String(s)) => s.to_str()?.to_string(),
50 _ => return inner.call::<Value>(args),
51 };
52 if !crate::lua::policy::env_visible(lua, &name) {
53 return Ok(Value::Nil);
54 }
55 inner.call::<Value>(args)
56 })?;
57 os_table.set("getenv", wrapper)?;
58 }
59 Ok(())
60}
61
62fn verb_target(verb: &'static str) -> Target {
63 Arc::new(move |args| arg_string(args, 0).map(|url| (verb.to_string(), url, 0)))
64}
65
66fn client_request_target() -> Target {
67 Arc::new(|args| Some((arg_string(args, 1)?, arg_string(args, 2)?, 2)))
68}
69
70fn arg_string(args: &MultiValue, at: usize) -> Option<String> {
71 match args.iter().nth(at) {
72 Some(Value::String(s)) => s.to_str().ok().map(|s| s.to_string()),
73 _ => None,
74 }
75}
76
77fn wrap(lua: &Lua, http: &Table, name: &str, target: Target) -> mlua::Result<()> {
78 let Value::Function(inner) = http.get::<Value>(name)? else {
79 return Ok(());
80 };
81 let wrapper = lua.create_async_function(move |lua, args: MultiValue| {
82 let inner = inner.clone();
83 let target = Arc::clone(&target);
84 async move {
85 let mut args = args;
86 if let Some((method, url, url_index)) = target(&args) {
87 credential::reject_in_url(&url)?;
88 guard_http(&lua, &method, &url)?;
89 args = fill_credentials(&lua, args, url_index)?;
90 }
91 let result = inner.call_async::<Value>(args).await?;
92 sanitize(&lua, result)
93 }
94 })?;
95 http.set(name, wrapper)
96}
97
98fn fill_credentials(lua: &Lua, args: MultiValue, url_index: usize) -> mlua::Result<MultiValue> {
102 let Some(policy) = active(lua) else {
103 return Ok(args);
104 };
105 if policy.credentials.is_empty() {
106 return Ok(args);
107 }
108 let mut out = Vec::with_capacity(args.len());
109 for (i, value) in args.into_iter().enumerate() {
110 out.push(if i == url_index {
111 value
112 } else {
113 credential::substitute(lua, &policy, value)?
114 });
115 }
116 Ok(MultiValue::from_iter(out))
117}
118
119fn sanitize(lua: &Lua, result: Value) -> mlua::Result<Value> {
123 let Value::Table(table) = &result else {
124 return Ok(result);
125 };
126 if let Some(limit) = response_limit(lua)
127 && let Ok(body) = table.get::<mlua::String>("body")
128 && body.as_bytes().len() > limit
129 {
130 return Err(mlua::Error::runtime(format!(
131 "policy: response body exceeds max_response_bytes ({limit})"
132 )));
133 }
134
135 let keys = redact_keys(lua);
136 if keys.is_empty() {
137 return Ok(result);
138 }
139 redact_body(lua, table, &keys)?;
140 redact_headers(table, &keys)?;
141 Ok(result)
142}
143
144fn redact_body(lua: &Lua, table: &Table, keys: &[String]) -> mlua::Result<()> {
145 let Ok(body) = table.get::<mlua::String>("body") else {
146 return Ok(());
147 };
148 let bytes = body.as_bytes();
149 let Some(redacted) = std::str::from_utf8(&bytes)
150 .ok()
151 .and_then(|text| redact_json_text(text, keys))
152 else {
153 return Ok(());
154 };
155 table.set("body", lua.create_string(redacted.as_bytes())?)
156}
157
158fn redact_headers(table: &Table, keys: &[String]) -> mlua::Result<()> {
159 let Ok(headers) = table.get::<Table>("headers") else {
160 return Ok(());
161 };
162 let names: Vec<String> = headers
163 .clone()
164 .pairs::<String, Value>()
165 .filter_map(|pair| pair.ok().map(|(name, _)| name))
166 .filter(|name| is_redacted_header(name, keys))
167 .collect();
168 for name in names {
169 headers.set(name, "[redacted]")?;
170 }
171 Ok(())
172}