1#![allow(clippy::module_name_repetitions)]
21
22use super::{utils::DetailedError, Context, Entities, EntityUid, PolicySet, Schema};
23use serde::{Deserialize, Serialize};
24#[cfg(feature = "wasm")]
25use wasm_bindgen::prelude::wasm_bindgen;
26
27#[cfg(feature = "wasm")]
28extern crate tsify;
29
30#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParsePolicySet"))]
32pub fn check_parse_policy_set(policies: PolicySet) -> CheckParseAnswer {
33 policies.parse().into()
34}
35
36pub fn check_parse_policy_set_json(
44 json: serde_json::Value,
45) -> Result<serde_json::Value, serde_json::Error> {
46 let ans = check_parse_policy_set(serde_json::from_value(json)?);
47 serde_json::to_value(ans)
48}
49
50pub fn check_parse_policy_set_json_str(json: &str) -> Result<String, serde_json::Error> {
59 let ans = check_parse_policy_set(serde_json::from_str(json)?);
60 serde_json::to_string(&ans)
61}
62
63#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseSchema"))]
65pub fn check_parse_schema(schema: Schema) -> CheckParseAnswer {
66 schema.parse().into()
67}
68
69pub fn check_parse_schema_json(
76 json: serde_json::Value,
77) -> Result<serde_json::Value, serde_json::Error> {
78 let ans = check_parse_schema(serde_json::from_value(json)?);
79 serde_json::to_value(ans)
80}
81
82pub fn check_parse_schema_json_str(json: &str) -> Result<String, serde_json::Error> {
91 let ans = check_parse_schema(serde_json::from_str(json)?);
92 serde_json::to_string(&ans)
93}
94
95#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseEntities"))]
97pub fn check_parse_entities(call: EntitiesParsingCall) -> CheckParseAnswer {
98 let schema = match call.schema.map(|s| s.parse().map(|res| res.0)).transpose() {
99 Ok(schema) => schema,
100 Err(err) => {
101 return CheckParseAnswer::Failure {
102 errors: vec![err.into()],
103 };
104 }
105 };
106 call.entities.parse(schema.as_ref()).into()
107}
108
109pub fn check_parse_entities_json(
118 json: serde_json::Value,
119) -> Result<serde_json::Value, serde_json::Error> {
120 let ans = check_parse_entities(serde_json::from_value(json)?);
121 serde_json::to_value(ans)
122}
123
124pub fn check_parse_entities_json_str(json: &str) -> Result<String, serde_json::Error> {
133 let ans = check_parse_entities(serde_json::from_str(json)?);
134 serde_json::to_string(&ans)
135}
136
137#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseContext"))]
139pub fn check_parse_context(call: ContextParsingCall) -> CheckParseAnswer {
140 let action = match call.action.map(|a| a.parse(Some("action"))).transpose() {
141 Ok(action) => action,
142 Err(err) => {
143 return CheckParseAnswer::Failure {
144 errors: vec![err.into()],
145 };
146 }
147 };
148 let schema = match call.schema.map(|s| s.parse().map(|res| res.0)).transpose() {
149 Ok(schema) => schema,
150 Err(err) => {
151 return CheckParseAnswer::Failure {
152 errors: vec![err.into()],
153 };
154 }
155 };
156 call.context.parse(schema.as_ref(), action.as_ref()).into()
157}
158
159pub fn check_parse_context_json(
167 json: serde_json::Value,
168) -> Result<serde_json::Value, serde_json::Error> {
169 let ans = check_parse_context(serde_json::from_value(json)?);
170 serde_json::to_value(ans)
171}
172
173pub fn check_parse_context_json_str(json: &str) -> Result<String, serde_json::Error> {
182 let ans = check_parse_context(serde_json::from_str(json)?);
183 serde_json::to_string(&ans)
184}
185
186#[derive(Debug, Serialize, Deserialize)]
188#[serde(tag = "type")]
189#[serde(rename_all = "camelCase")]
190#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
191#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
192pub enum CheckParseAnswer {
193 Success,
195 Failure {
197 errors: Vec<DetailedError>,
199 },
200}
201
202impl<T> From<Result<T, miette::Report>> for CheckParseAnswer {
203 fn from(res: Result<T, miette::Report>) -> Self {
204 match res {
205 Ok(_) => Self::Success,
206 Err(err) => Self::Failure {
207 errors: vec![err.into()],
208 },
209 }
210 }
211}
212
213impl<T> From<Result<T, Vec<miette::Report>>> for CheckParseAnswer {
214 fn from(res: Result<T, Vec<miette::Report>>) -> Self {
215 match res {
216 Ok(_) => Self::Success,
217 Err(errs) => Self::Failure {
218 errors: errs.into_iter().map(Into::into).collect(),
219 },
220 }
221 }
222}
223
224#[derive(Serialize, Deserialize, Debug)]
226#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
227#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
228#[serde(rename_all = "camelCase")]
229#[serde(deny_unknown_fields)]
230pub struct EntitiesParsingCall {
231 entities: Entities,
233 #[serde(default)]
235 schema: Option<Schema>,
236}
237
238#[derive(Serialize, Deserialize, Debug)]
240#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
241#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
242#[serde(rename_all = "camelCase")]
243#[serde(deny_unknown_fields)]
244pub struct ContextParsingCall {
245 context: Context,
247 #[serde(default)]
249 schema: Option<Schema>,
250 #[serde(default)]
252 action: Option<EntityUid>,
253}
254
255#[cfg(test)]
256mod test {
257 use super::*;
258 use crate::ffi::test_utils::assert_exactly_one_error;
259 use cool_asserts::assert_matches;
260 use serde_json::json;
261
262 #[track_caller]
263 fn assert_check_parse_is_ok(parse_result: &CheckParseAnswer) {
264 assert_matches!(parse_result, CheckParseAnswer::Success);
265 }
266
267 #[track_caller]
268 fn assert_check_parse_is_err(parse_result: &CheckParseAnswer) -> &[DetailedError] {
269 assert_matches!(
270 parse_result,
271 CheckParseAnswer::Failure { errors } => errors
272 )
273 }
274
275 #[test]
276 fn can_parse_1_policy() {
277 let call = json!({
278 "staticPolicies": "permit(principal, action, resource);"
279 });
280 let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
281 assert_check_parse_is_ok(&answer);
282 }
283
284 #[test]
285 fn can_parse_multi_policy() {
286 let call = json!({
287 "staticPolicies": "forbid(principal, action, resource); permit(principal == User::\"alice\", action == Action::\"view\", resource in Albums::\"alice_albums\");"
288 });
289 let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
290 assert_check_parse_is_ok(&answer);
291 }
292
293 #[test]
294 fn parse_policy_set_fails() {
295 let call = json!({
296 "staticPolicies": "forbid(principal, action, resource);permit(2pac, action, resource)"
297 });
298 let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
299 let errs = assert_check_parse_is_err(&answer);
300 assert_exactly_one_error(
301 errs,
302 "failed to parse policies from string: unexpected token `2`",
303 None,
304 );
305 }
306
307 #[test]
308 fn can_parse_template() {
309 let call = json!({
310 "templates": {
311 "ID0": "permit (principal == ?principal, action, resource == ?resource);"
312 }
313 });
314 let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
315 assert_check_parse_is_ok(&answer);
316 }
317
318 #[test]
319 fn check_parse_schema_succeeds_empty_schema() {
320 let call = json!({});
321 let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
322 assert_check_parse_is_ok(&answer);
323 }
324
325 #[test]
326 fn check_parse_schema_succeeds_basic_schema() {
327 let call = json!({
328 "MyNamespace": {
329 "entityTypes": {},
330 "actions": {}
331 }
332 });
333 let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
334 assert_check_parse_is_ok(&answer);
335 }
336
337 #[test]
338 fn check_parse_schema_fails() {
339 let call = json!({
340 "MyNamespace": {
341 "entityTypes": {}
342 }
343 });
344 let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
345 let errs = assert_check_parse_is_err(&answer);
346 assert_exactly_one_error(
347 errs,
348 "failed to parse schema from JSON: missing field `actions`",
349 None,
350 );
351 }
352
353 #[test]
354 fn check_parse_entities_succeeds() {
355 let call = json!({
356 "entities": [
357 {
358 "uid": {
359 "type": "TheNamespace::User",
360 "id": "alice"
361 },
362 "attrs": {
363 "department": "HardwareEngineering",
364 "jobLevel": 5
365 },
366 "parents": []
367 }
368 ],
369 "schema": {
370 "TheNamespace": {
371 "entityTypes": {
372 "User": {
373 "memberOfTypes": [],
374 "shape": {
375 "attributes": {
376 "department": {
377 "type": "String"
378 },
379 "jobLevel": {
380 "type": "Long"
381 }
382 },
383 "type": "Record"
384 }
385 }
386 },
387 "actions": {}
388 }
389 }
390 });
391 let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
392 assert_check_parse_is_ok(&answer);
393 }
394
395 #[test]
396 fn check_parse_entities_succeeds_with_no_schema() {
397 let call = json!({
398 "entities": [
399 {
400 "uid": {
401 "type": "TheNamespace::User",
402 "id": "alice"
403 },
404 "attrs": {
405 "department": "HardwareEngineering",
406 "jobLevel": 5
407 },
408 "parents": []
409 }
410 ]
411 });
412 let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
413 assert_check_parse_is_ok(&answer);
414 }
415
416 #[test]
417 fn check_parse_entities_fails_on_bad_entity() {
418 let call = json!({
419 "entities": [
420 {
421 "uid": "TheNamespace::User::\"alice\"",
422 "attrs": {
423 "benchPress": "doesn'tevenlift"
424 },
425 "parents": []
426 }
427 ],
428 "schema": {
429 "TheNamespace": {
430 "entityTypes": {
431 "User": {
432 "memberOfTypes": [],
433 "shape": {
434 "attributes": {
435 "department": {
436 "type": "String"
437 }
438 },
439 "type": "Record"
440 }
441 }
442 },
443 "actions": {}
444 }
445 }
446 });
447 let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
448 let errs = assert_check_parse_is_err(&answer);
449 assert_exactly_one_error(
450 errs,
451 "error during entity deserialization: in uid field of <unknown entity>, expected a literal entity reference, but got `\"TheNamespace::User::\\\"alice\\\"\"`",
452 Some("literal entity references can be made with `{ \"type\": \"SomeType\", \"id\": \"SomeId\" }`")
453 );
454 }
455
456 #[test]
457 fn check_parse_context_succeeds() {
458 let call = json!({
459 "context": {
460 "referrer": "Morpheus"
461 },
462 "action": {
463 "type": "Ex::Action",
464 "id": "Join"
465 },
466 "schema": {
467 "Ex": {
468 "entityTypes": {
469 "User": {},
470 "Folder": {}
471 },
472 "actions": {
473 "Join": {
474 "appliesTo": {
475 "principalTypes": ["User"],
476 "resourceTypes": ["Folder"],
477 "context": {
478 "type": "Record",
479 "attributes": {
480 "referrer": {
481 "type": "String",
482 "required": true
483 }
484 }
485 }
486 }
487 }
488 }
489 }
490 }
491
492 });
493 let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap();
494 assert_check_parse_is_ok(&answer);
495 }
496
497 #[test]
498 fn check_parse_context_fails_for_bad_context() {
499 let call = json!({
500 "context": {
501 "wrongAttr": true
502 },
503 "action": {
504 "type": "Ex::Action",
505 "id": "Join"
506 },
507 "schema": {
508 "Ex": {
509 "entityTypes": {
510 "User": {},
511 "Folder": {}
512 },
513 "actions": {
514 "Join": {
515 "appliesTo": {
516 "principalTypes" : ["User"],
517 "resourceTypes": ["Folder"],
518 "context": {
519 "type": "Record",
520 "attributes": {
521 "referrer": {
522 "type": "String",
523 "required": true
524 }
525 }
526 }
527 }
528 }
529 }
530 }
531 }
532 });
533 let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap();
534 let errs = assert_check_parse_is_err(&answer);
535 assert_exactly_one_error(errs, "while parsing context, expected the record to have an attribute `referrer`, but it does not", None);
536 }
537}