1#![allow(clippy::needless_return)]
21
22use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
23use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
24use serde::{Deserialize, Serialize};
25use std::io::Write;
26use std::{
27 collections::HashMap,
28 fmt::{self, Display},
29 fs::OpenOptions,
30 path::{Path, PathBuf},
31 process::{ExitCode, Termination},
32 str::FromStr,
33 time::Instant,
34};
35
36use cedar_policy::*;
37use cedar_policy_formatter::{policies_str_to_pretty, Config};
38
39#[derive(Parser, Debug)]
41#[command(author, version, about, long_about = None)] pub struct Cli {
43 #[command(subcommand)]
44 pub command: Commands,
45 #[arg(
47 global = true,
48 short = 'f',
49 long = "error-format",
50 env = "CEDAR_ERROR_FORMAT",
51 default_value_t,
52 value_enum
53 )]
54 pub err_fmt: ErrorFormat,
55}
56
57#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
58pub enum ErrorFormat {
59 #[default]
62 Human,
63 Plain,
66 Json,
68}
69
70impl Display for ErrorFormat {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(
73 f,
74 "{}",
75 match self {
76 ErrorFormat::Human => "human",
77 ErrorFormat::Plain => "plain",
78 ErrorFormat::Json => "json",
79 }
80 )
81 }
82}
83
84#[derive(Subcommand, Debug)]
85pub enum Commands {
86 Authorize(AuthorizeArgs),
88 Evaluate(EvaluateArgs),
90 Validate(ValidateArgs),
92 CheckParse(CheckParseArgs),
97 Link(LinkArgs),
99 Format(FormatArgs),
101 TranslatePolicy(TranslatePolicyArgs),
103 TranslateSchema(TranslateSchemaArgs),
105 Visualize(VisualizeArgs),
108 New(NewArgs),
110 PartiallyAuthorize(PartiallyAuthorizeArgs),
112 LanguageVersion,
114}
115
116#[derive(Args, Debug)]
117pub struct TranslatePolicyArgs {
118 #[arg(long)]
120 pub direction: PolicyTranslationDirection,
121 #[arg(short = 'p', long = "policies", value_name = "FILE")]
124 pub input_file: Option<String>,
125}
126
127#[derive(Debug, Clone, Copy, ValueEnum)]
129pub enum PolicyTranslationDirection {
130 CedarToJson,
132 JsonToCedar,
134}
135
136#[derive(Args, Debug)]
137pub struct TranslateSchemaArgs {
138 #[arg(long)]
140 pub direction: SchemaTranslationDirection,
141 #[arg(short = 's', long = "schema", value_name = "FILE")]
144 pub input_file: Option<String>,
145}
146
147#[derive(Debug, Clone, Copy, ValueEnum)]
149pub enum SchemaTranslationDirection {
150 JsonToCedar,
152 CedarToJson,
154}
155
156#[derive(Debug, Default, Clone, Copy, ValueEnum)]
157pub enum SchemaFormat {
158 #[default]
160 Cedar,
161 Json,
163}
164
165#[derive(Debug, Clone, Copy, ValueEnum)]
166pub enum ValidationMode {
167 Strict,
169 Permissive,
171 Partial,
173}
174
175#[derive(Args, Debug)]
176pub struct ValidateArgs {
177 #[command(flatten)]
179 pub schema: SchemaArgs,
180 #[command(flatten)]
182 pub policies: PoliciesArgs,
183 #[arg(long)]
185 pub deny_warnings: bool,
186 #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
191 pub validation_mode: ValidationMode,
192 #[arg(long)]
194 pub level: Option<u32>,
195}
196
197#[derive(Args, Debug)]
198pub struct CheckParseArgs {
199 #[command(flatten)]
201 pub policies: OptionalPoliciesArgs,
202 #[command(flatten)]
204 pub schema: OptionalSchemaArgs,
205 #[arg(long = "entities", value_name = "FILE")]
207 pub entities_file: Option<PathBuf>,
208}
209
210#[derive(Args, Debug)]
212pub struct RequestArgs {
213 #[arg(short = 'l', long)]
215 pub principal: Option<String>,
216 #[arg(short, long)]
218 pub action: Option<String>,
219 #[arg(short, long)]
221 pub resource: Option<String>,
222 #[arg(short, long = "context", value_name = "FILE")]
225 pub context_json_file: Option<String>,
226 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
231 pub request_json_file: Option<String>,
232 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
235 pub request_validation: bool,
236}
237
238#[cfg(feature = "partial-eval")]
239#[derive(Args, Debug)]
241pub struct PartialRequestArgs {
242 #[arg(short = 'l', long)]
244 pub principal: Option<String>,
245 #[arg(short, long)]
247 pub action: Option<String>,
248 #[arg(short, long)]
250 pub resource: Option<String>,
251 #[arg(short, long = "context", value_name = "FILE")]
254 pub context_json_file: Option<String>,
255 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
260 pub request_json_file: Option<String>,
261}
262
263impl RequestArgs {
264 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
271 match &self.request_json_file {
272 Some(jsonfile) => {
273 let jsonstring = std::fs::read_to_string(jsonfile)
274 .into_diagnostic()
275 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
276 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
277 .into_diagnostic()
278 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
279 let principal = qjson.principal.parse().wrap_err_with(|| {
280 format!("failed to parse principal in {jsonfile} as entity Uid")
281 })?;
282 let action = qjson.action.parse().wrap_err_with(|| {
283 format!("failed to parse action in {jsonfile} as entity Uid")
284 })?;
285 let resource = qjson.resource.parse().wrap_err_with(|| {
286 format!("failed to parse resource in {jsonfile} as entity Uid")
287 })?;
288 let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
289 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
290 Request::new(
291 principal,
292 action,
293 resource,
294 context,
295 if self.request_validation {
296 schema
297 } else {
298 None
299 },
300 )
301 .map_err(|e| miette!("{e}"))
302 }
303 None => {
304 let principal = self
305 .principal
306 .as_ref()
307 .map(|s| {
308 s.parse().wrap_err_with(|| {
309 format!("failed to parse principal {s} as entity Uid")
310 })
311 })
312 .transpose()?;
313 let action = self
314 .action
315 .as_ref()
316 .map(|s| {
317 s.parse()
318 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
319 })
320 .transpose()?;
321 let resource = self
322 .resource
323 .as_ref()
324 .map(|s| {
325 s.parse()
326 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
327 })
328 .transpose()?;
329 let context: Context = match &self.context_json_file {
330 None => Context::empty(),
331 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
332 Ok(f) => Context::from_json_file(
333 f,
334 schema.and_then(|s| Some((s, action.as_ref()?))),
335 )
336 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
337 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
338 format!("error while loading context from {jsonfile}")
339 })?,
340 },
341 };
342 match (principal, action, resource) {
343 (Some(principal), Some(action), Some(resource)) => Request::new(
344 principal,
345 action,
346 resource,
347 context,
348 if self.request_validation {
349 schema
350 } else {
351 None
352 },
353 )
354 .map_err(|e| miette!("{e}")),
355 _ => Err(miette!(
356 "All three (`principal`, `action`, `resource`) variables must be specified"
357 )),
358 }
359 }
360 }
361 }
362}
363
364#[cfg(feature = "partial-eval")]
365impl PartialRequestArgs {
366 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
367 let mut builder = RequestBuilder::default();
368 let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
369 Some(jsonfile) => {
370 let jsonstring = std::fs::read_to_string(jsonfile)
371 .into_diagnostic()
372 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
373 serde_json::from_str(&jsonstring)
374 .into_diagnostic()
375 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
376 }
377 None => PartialRequestJSON {
378 principal: self.principal.clone(),
379 action: self.action.clone(),
380 resource: self.resource.clone(),
381 context: self
382 .context_json_file
383 .as_ref()
384 .map(|jsonfile| {
385 let jsonstring = std::fs::read_to_string(jsonfile)
386 .into_diagnostic()
387 .wrap_err_with(|| {
388 format!("failed to open context-json file {jsonfile}")
389 })?;
390 serde_json::from_str(&jsonstring)
391 .into_diagnostic()
392 .wrap_err_with(|| {
393 format!("failed to parse context-json file {jsonfile}")
394 })
395 })
396 .transpose()?,
397 },
398 };
399
400 if let Some(principal) = qjson
401 .principal
402 .map(|s| {
403 s.parse()
404 .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
405 })
406 .transpose()?
407 {
408 builder = builder.principal(principal);
409 }
410
411 let action = qjson
412 .action
413 .map(|s| {
414 s.parse::<EntityUid>()
415 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
416 })
417 .transpose()?;
418
419 if let Some(action_ref) = &action {
420 builder = builder.action(action_ref.clone());
421 }
422
423 if let Some(resource) = qjson
424 .resource
425 .map(|s| {
426 s.parse()
427 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
428 })
429 .transpose()?
430 {
431 builder = builder.resource(resource);
432 }
433
434 if let Some(context) = qjson
435 .context
436 .map(|json| {
437 Context::from_json_value(
438 json.clone(),
439 schema.and_then(|s| Some((s, action.as_ref()?))),
440 )
441 .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
442 })
443 .transpose()?
444 {
445 builder = builder.context(context);
446 }
447
448 if let Some(schema) = schema {
449 builder
450 .schema(schema)
451 .build()
452 .wrap_err_with(|| "failed to build request with validation".to_string())
453 } else {
454 Ok(builder.build())
455 }
456 }
457}
458
459#[derive(Args, Debug)]
461pub struct PoliciesArgs {
462 #[arg(short, long = "policies", value_name = "FILE")]
464 pub policies_file: Option<String>,
465 #[arg(long = "policy-format", default_value_t, value_enum)]
467 pub policy_format: PolicyFormat,
468 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
470 pub template_linked_file: Option<String>,
471}
472
473impl PoliciesArgs {
474 fn get_policy_set(&self) -> Result<PolicySet> {
476 let mut pset = match self.policy_format {
477 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
478 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
479 }?;
480 if let Some(links_filename) = self.template_linked_file.as_ref() {
481 add_template_links_to_set(links_filename, &mut pset)?;
482 }
483 Ok(pset)
484 }
485}
486
487#[derive(Args, Debug)]
490pub struct OptionalPoliciesArgs {
491 #[arg(short, long = "policies", value_name = "FILE")]
493 pub policies_file: Option<String>,
494 #[arg(long = "policy-format", default_value_t, value_enum)]
496 pub policy_format: PolicyFormat,
497 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
500 pub template_linked_file: Option<String>,
501}
502
503impl OptionalPoliciesArgs {
504 fn get_policy_set(&self) -> Result<Option<PolicySet>> {
507 match &self.policies_file {
508 None => Ok(None),
509 Some(policies_file) => {
510 let pargs = PoliciesArgs {
511 policies_file: Some(policies_file.clone()),
512 policy_format: self.policy_format,
513 template_linked_file: self.template_linked_file.clone(),
514 };
515 pargs.get_policy_set().map(Some)
516 }
517 }
518 }
519}
520
521#[derive(Args, Debug)]
523pub struct SchemaArgs {
524 #[arg(short, long = "schema", value_name = "FILE")]
526 pub schema_file: PathBuf,
527 #[arg(long, value_enum, default_value_t)]
529 pub schema_format: SchemaFormat,
530}
531
532impl SchemaArgs {
533 fn get_schema(&self) -> Result<Schema> {
535 read_schema_from_file(&self.schema_file, self.schema_format)
536 }
537}
538
539#[derive(Args, Debug)]
542pub struct OptionalSchemaArgs {
543 #[arg(short, long = "schema", value_name = "FILE")]
545 pub schema_file: Option<PathBuf>,
546 #[arg(long, value_enum, default_value_t)]
548 pub schema_format: SchemaFormat,
549}
550
551impl OptionalSchemaArgs {
552 fn get_schema(&self) -> Result<Option<Schema>> {
554 let Some(schema_file) = &self.schema_file else {
555 return Ok(None);
556 };
557 read_schema_from_file(schema_file, self.schema_format).map(Some)
558 }
559}
560
561fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
562 let path = path.as_ref();
563 let schema_src = read_from_file(path, "schema")?;
564 match format {
565 SchemaFormat::Json => Schema::from_json_str(&schema_src)
566 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
567 SchemaFormat::Cedar => {
568 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
569 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
570 for warning in warnings {
571 let report = miette::Report::new(warning);
572 eprintln!("{:?}", report);
573 }
574 Ok(schema)
575 }
576 }
577}
578
579#[derive(Args, Debug)]
580pub struct AuthorizeArgs {
581 #[command(flatten)]
583 pub request: RequestArgs,
584 #[command(flatten)]
586 pub policies: PoliciesArgs,
587 #[command(flatten)]
592 pub schema: OptionalSchemaArgs,
593 #[arg(long = "entities", value_name = "FILE")]
595 pub entities_file: String,
596 #[arg(short, long)]
598 pub verbose: bool,
599 #[arg(short, long)]
601 pub timing: bool,
602}
603
604#[cfg(feature = "partial-eval")]
605#[derive(Args, Debug)]
606pub struct PartiallyAuthorizeArgs {
607 #[command(flatten)]
609 pub request: PartialRequestArgs,
610 #[command(flatten)]
612 pub policies: PoliciesArgs,
613 #[command(flatten)]
618 pub schema: OptionalSchemaArgs,
619 #[arg(long = "entities", value_name = "FILE")]
621 pub entities_file: String,
622 #[arg(short, long)]
624 pub timing: bool,
625}
626
627#[cfg(not(feature = "partial-eval"))]
628#[derive(Debug, Args)]
629pub struct PartiallyAuthorizeArgs;
630
631#[derive(Args, Debug)]
632pub struct VisualizeArgs {
633 #[arg(long = "entities", value_name = "FILE")]
634 pub entities_file: String,
635}
636
637#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
638pub enum PolicyFormat {
639 #[default]
641 Cedar,
642 Json,
644}
645
646#[derive(Args, Debug)]
647pub struct LinkArgs {
648 #[command(flatten)]
650 pub policies: PoliciesArgs,
651 #[arg(long)]
653 pub template_id: String,
654 #[arg(short, long)]
656 pub new_id: String,
657 #[arg(short, long)]
659 pub arguments: Arguments,
660}
661
662#[derive(Args, Debug)]
663pub struct FormatArgs {
664 #[arg(short, long = "policies", value_name = "FILE")]
666 pub policies_file: Option<String>,
667
668 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
670 pub line_width: usize,
671
672 #[arg(short, long, value_name = "INT", default_value_t = 2)]
674 pub indent_width: isize,
675
676 #[arg(short, long, group = "action", requires = "policies_file")]
678 pub write: bool,
679
680 #[arg(short, long, group = "action")]
682 pub check: bool,
683}
684
685#[derive(Args, Debug)]
686pub struct NewArgs {
687 #[arg(short, long, value_name = "DIR")]
689 pub name: String,
690}
691
692#[derive(Clone, Debug, Deserialize)]
694#[serde(try_from = "HashMap<String,String>")]
695pub struct Arguments {
696 pub data: HashMap<SlotId, String>,
697}
698
699impl TryFrom<HashMap<String, String>> for Arguments {
700 type Error = String;
701
702 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
703 Ok(Self {
704 data: value
705 .into_iter()
706 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
707 .collect::<Result<HashMap<SlotId, String>, String>>()?,
708 })
709 }
710}
711
712impl FromStr for Arguments {
713 type Err = serde_json::Error;
714
715 fn from_str(s: &str) -> Result<Self, Self::Err> {
716 serde_json::from_str(s)
717 }
718}
719
720#[derive(Deserialize)]
722struct RequestJSON {
723 #[serde(default)]
725 principal: String,
726 #[serde(default)]
728 action: String,
729 #[serde(default)]
731 resource: String,
732 context: serde_json::Value,
734}
735
736#[cfg(feature = "partial-eval")]
737#[derive(Deserialize)]
739struct PartialRequestJSON {
740 pub(self) principal: Option<String>,
742 pub(self) action: Option<String>,
744 pub(self) resource: Option<String>,
746 pub(self) context: Option<serde_json::Value>,
748}
749
750#[derive(Args, Debug)]
751pub struct EvaluateArgs {
752 #[command(flatten)]
754 pub request: RequestArgs,
755 #[command(flatten)]
760 pub schema: OptionalSchemaArgs,
761 #[arg(long = "entities", value_name = "FILE")]
764 pub entities_file: Option<String>,
765 #[arg(value_name = "EXPRESSION")]
767 pub expression: String,
768}
769
770#[derive(Eq, PartialEq, Debug, Copy, Clone)]
771pub enum CedarExitCode {
772 Success,
775 Failure,
777 AuthorizeDeny,
780 ValidationFailure,
783 #[cfg(feature = "partial-eval")]
784 Unknown,
787}
788
789impl Termination for CedarExitCode {
790 fn report(self) -> ExitCode {
791 match self {
792 CedarExitCode::Success => ExitCode::SUCCESS,
793 CedarExitCode::Failure => ExitCode::FAILURE,
794 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
795 CedarExitCode::ValidationFailure => ExitCode::from(3),
796 #[cfg(feature = "partial-eval")]
797 CedarExitCode::Unknown => ExitCode::SUCCESS,
798 }
799 }
800}
801
802pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
803 if (
806 &args.policies.policies_file,
807 &args.schema.schema_file,
808 &args.entities_file,
809 ) == (&None, &None, &None)
810 {
811 let pargs = PoliciesArgs {
812 policies_file: None, policy_format: args.policies.policy_format,
814 template_linked_file: args.policies.template_linked_file.clone(),
815 };
816 match pargs.get_policy_set() {
817 Ok(_) => return CedarExitCode::Success,
818 Err(e) => {
819 println!("{e:?}");
820 return CedarExitCode::Failure;
821 }
822 }
823 }
824
825 let mut exit_code = CedarExitCode::Success;
826 match args.policies.get_policy_set() {
827 Ok(_) => (),
828 Err(e) => {
829 println!("{e:?}");
830 exit_code = CedarExitCode::Failure;
831 }
832 }
833 let schema = match args.schema.get_schema() {
834 Ok(schema) => schema,
835 Err(e) => {
836 println!("{e:?}");
837 exit_code = CedarExitCode::Failure;
838 None
839 }
840 };
841 match &args.entities_file {
842 None => (),
843 Some(efile) => match load_entities(efile, schema.as_ref()) {
844 Ok(_) => (),
845 Err(e) => {
846 println!("{e:?}");
847 exit_code = CedarExitCode::Failure;
848 }
849 },
850 }
851 exit_code
852}
853
854pub fn validate(args: &ValidateArgs) -> CedarExitCode {
855 let mode = match args.validation_mode {
856 ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
857 ValidationMode::Permissive => {
858 #[cfg(not(feature = "permissive-validate"))]
859 {
860 eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
861 return CedarExitCode::Failure;
862 }
863 #[cfg(feature = "permissive-validate")]
864 cedar_policy::ValidationMode::Permissive
865 }
866 ValidationMode::Partial => {
867 #[cfg(not(feature = "partial-validate"))]
868 {
869 eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
870 return CedarExitCode::Failure;
871 }
872 #[cfg(feature = "partial-validate")]
873 cedar_policy::ValidationMode::Partial
874 }
875 };
876
877 let pset = match args.policies.get_policy_set() {
878 Ok(pset) => pset,
879 Err(e) => {
880 println!("{e:?}");
881 return CedarExitCode::Failure;
882 }
883 };
884
885 let schema = match args.schema.get_schema() {
886 Ok(schema) => schema,
887 Err(e) => {
888 println!("{e:?}");
889 return CedarExitCode::Failure;
890 }
891 };
892
893 let validator = Validator::new(schema);
894
895 let result = if let Some(level) = args.level {
896 validator.validate_with_level(&pset, mode, level)
897 } else {
898 validator.validate(&pset, mode)
899 };
900
901 if !result.validation_passed()
902 || (args.deny_warnings && !result.validation_passed_without_warnings())
903 {
904 println!(
905 "{:?}",
906 Report::new(result).wrap_err("policy set validation failed")
907 );
908 CedarExitCode::ValidationFailure
909 } else {
910 println!(
911 "{:?}",
912 Report::new(result).wrap_err("policy set validation passed")
913 );
914 CedarExitCode::Success
915 }
916}
917
918pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
919 println!();
920 let schema = match args.schema.get_schema() {
921 Ok(opt) => opt,
922 Err(e) => {
923 println!("{e:?}");
924 return (CedarExitCode::Failure, EvalResult::Bool(false));
925 }
926 };
927 let request = match args.request.get_request(schema.as_ref()) {
928 Ok(q) => q,
929 Err(e) => {
930 println!("{e:?}");
931 return (CedarExitCode::Failure, EvalResult::Bool(false));
932 }
933 };
934 let expr =
935 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
936 Ok(expr) => expr,
937 Err(e) => {
938 println!("{:?}", e.with_source_code(args.expression.clone()));
939 return (CedarExitCode::Failure, EvalResult::Bool(false));
940 }
941 };
942 let entities = match &args.entities_file {
943 None => Entities::empty(),
944 Some(file) => match load_entities(file, schema.as_ref()) {
945 Ok(entities) => entities,
946 Err(e) => {
947 println!("{e:?}");
948 return (CedarExitCode::Failure, EvalResult::Bool(false));
949 }
950 },
951 };
952 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
953 {
954 Err(e) => {
955 println!("{e:?}");
956 return (CedarExitCode::Failure, EvalResult::Bool(false));
957 }
958 Ok(result) => {
959 println!("{result}");
960 return (CedarExitCode::Success, result);
961 }
962 }
963}
964
965pub fn link(args: &LinkArgs) -> CedarExitCode {
966 if let Err(err) = link_inner(args) {
967 println!("{err:?}");
968 CedarExitCode::Failure
969 } else {
970 CedarExitCode::Success
971 }
972}
973
974pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
975 match load_entities(&args.entities_file, None) {
976 Ok(entities) => {
977 println!("{}", entities.to_dot_str());
978 CedarExitCode::Success
979 }
980 Err(report) => {
981 eprintln!("{report:?}");
982 CedarExitCode::Failure
983 }
984 }
985}
986
987fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
992 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
993 let config = Config {
994 line_width: args.line_width,
995 indent_width: args.indent_width,
996 };
997 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
998 let are_policies_equivalent = policies_str == formatted_policy;
999
1000 match &args.policies_file {
1001 Some(policies_file) if args.write => {
1002 let mut file = OpenOptions::new()
1003 .write(true)
1004 .truncate(true)
1005 .open(policies_file)
1006 .into_diagnostic()
1007 .wrap_err(format!("failed to open {policies_file} for writing"))?;
1008 file.write_all(formatted_policy.as_bytes())
1009 .into_diagnostic()
1010 .wrap_err(format!(
1011 "failed to write formatted policies to {policies_file}"
1012 ))?;
1013 }
1014 _ => print!("{}", formatted_policy),
1015 }
1016 Ok(are_policies_equivalent)
1017}
1018
1019pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1020 match format_policies_inner(args) {
1021 Ok(false) if args.check => CedarExitCode::Failure,
1022 Err(err) => {
1023 println!("{err:?}");
1024 CedarExitCode::Failure
1025 }
1026 _ => CedarExitCode::Success,
1027 }
1028}
1029
1030fn translate_policy_to_cedar(
1031 json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1032) -> Result<String> {
1033 let policy_set = read_json_policy_set(json_src)?;
1034 policy_set.to_cedar().ok_or_else(|| {
1035 miette!("Unable to translate policy set containing template linked policies.")
1036 })
1037}
1038
1039fn translate_policy_to_json(
1040 cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1041) -> Result<String> {
1042 let policy_set = read_cedar_policy_set(cedar_src)?;
1043 let output = policy_set.to_json()?.to_string();
1044 Ok(output)
1045}
1046
1047fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1048 let translate = match args.direction {
1049 PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1050 PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1051 };
1052 translate(args.input_file.as_ref())
1053}
1054
1055pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1056 match translate_policy_inner(args) {
1057 Ok(sf) => {
1058 println!("{sf}");
1059 CedarExitCode::Success
1060 }
1061 Err(err) => {
1062 eprintln!("{err:?}");
1063 CedarExitCode::Failure
1064 }
1065 }
1066}
1067
1068fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1069 let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1070 let output = fragment.to_cedarschema()?;
1071 Ok(output)
1072}
1073
1074fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1075 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1076 for warning in warnings {
1077 let report = miette::Report::new(warning);
1078 eprintln!("{:?}", report);
1079 }
1080 let output = fragment.to_json_string()?;
1081 Ok(output)
1082}
1083
1084fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1085 let translate = match args.direction {
1086 SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1087 SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1088 };
1089 read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1090}
1091
1092pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1093 match translate_schema_inner(args) {
1094 Ok(sf) => {
1095 println!("{sf}");
1096 CedarExitCode::Success
1097 }
1098 Err(err) => {
1099 eprintln!("{err:?}");
1100 CedarExitCode::Failure
1101 }
1102 }
1103}
1104
1105fn generate_schema(path: &Path) -> Result<()> {
1107 std::fs::write(
1108 path,
1109 serde_json::to_string_pretty(&serde_json::json!(
1110 {
1111 "": {
1112 "entityTypes": {
1113 "A": {
1114 "memberOfTypes": [
1115 "B"
1116 ]
1117 },
1118 "B": {
1119 "memberOfTypes": []
1120 },
1121 "C": {
1122 "memberOfTypes": []
1123 }
1124 },
1125 "actions": {
1126 "action": {
1127 "appliesTo": {
1128 "resourceTypes": [
1129 "C"
1130 ],
1131 "principalTypes": [
1132 "A",
1133 "B"
1134 ]
1135 }
1136 }
1137 }
1138 }
1139 }))
1140 .into_diagnostic()?,
1141 )
1142 .into_diagnostic()
1143}
1144
1145fn generate_policy(path: &Path) -> Result<()> {
1146 std::fs::write(
1147 path,
1148 r#"permit (
1149 principal in A::"a",
1150 action == Action::"action",
1151 resource == C::"c"
1152) when { true };
1153"#,
1154 )
1155 .into_diagnostic()
1156}
1157
1158fn generate_entities(path: &Path) -> Result<()> {
1159 std::fs::write(
1160 path,
1161 serde_json::to_string_pretty(&serde_json::json!(
1162 [
1163 {
1164 "uid": { "type": "A", "id": "a"} ,
1165 "attrs": {},
1166 "parents": [{"type": "B", "id": "b"}]
1167 },
1168 {
1169 "uid": { "type": "B", "id": "b"} ,
1170 "attrs": {},
1171 "parents": []
1172 },
1173 {
1174 "uid": { "type": "C", "id": "c"} ,
1175 "attrs": {},
1176 "parents": []
1177 }
1178 ]))
1179 .into_diagnostic()?,
1180 )
1181 .into_diagnostic()
1182}
1183
1184fn new_inner(args: &NewArgs) -> Result<()> {
1185 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1186 std::fs::create_dir(dir).into_diagnostic()?;
1187 let schema_path = dir.join("schema.cedarschema.json");
1188 let policy_path = dir.join("policy.cedar");
1189 let entities_path = dir.join("entities.json");
1190 generate_schema(&schema_path)?;
1191 generate_policy(&policy_path)?;
1192 generate_entities(&entities_path)
1193}
1194
1195pub fn new(args: &NewArgs) -> CedarExitCode {
1196 if let Err(err) = new_inner(args) {
1197 println!("{err:?}");
1198 CedarExitCode::Failure
1199 } else {
1200 CedarExitCode::Success
1201 }
1202}
1203
1204pub fn language_version() -> CedarExitCode {
1205 let version = get_lang_version();
1206 println!(
1207 "Cedar language version: {}.{}",
1208 version.major, version.minor
1209 );
1210 CedarExitCode::Success
1211}
1212
1213fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1214 data.iter()
1215 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1216 .collect::<Result<HashMap<SlotId, EntityUid>>>()
1217}
1218
1219fn link_inner(args: &LinkArgs) -> Result<()> {
1220 let mut policies = args.policies.get_policy_set()?;
1221 let slotenv = create_slot_env(&args.arguments.data)?;
1222 policies.link(
1223 PolicyId::new(&args.template_id),
1224 PolicyId::new(&args.new_id),
1225 slotenv,
1226 )?;
1227 let linked = policies
1228 .policy(&PolicyId::new(&args.new_id))
1229 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1230 println!("Template-linked policy added: {linked}");
1231
1232 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1234 update_template_linked_file(
1235 links_filename,
1236 TemplateLinked {
1237 template_id: args.template_id.clone(),
1238 link_id: args.new_id.clone(),
1239 args: args.arguments.data.clone(),
1240 },
1241 )?;
1242 }
1243
1244 Ok(())
1245}
1246
1247#[derive(Clone, Serialize, Deserialize, Debug)]
1248#[serde(try_from = "LiteralTemplateLinked")]
1249#[serde(into = "LiteralTemplateLinked")]
1250struct TemplateLinked {
1251 template_id: String,
1252 link_id: String,
1253 args: HashMap<SlotId, String>,
1254}
1255
1256impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1257 type Error = String;
1258
1259 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1260 Ok(Self {
1261 template_id: value.template_id,
1262 link_id: value.link_id,
1263 args: value
1264 .args
1265 .into_iter()
1266 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1267 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1268 })
1269 }
1270}
1271
1272fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1273 match s.as_ref() {
1274 "?principal" => Ok(SlotId::principal()),
1275 "?resource" => Ok(SlotId::resource()),
1276 _ => Err(format!(
1277 "Invalid SlotId! Expected ?principal|?resource, got: {}",
1278 s.as_ref()
1279 )),
1280 }
1281}
1282
1283#[derive(Serialize, Deserialize)]
1284struct LiteralTemplateLinked {
1285 template_id: String,
1286 link_id: String,
1287 args: HashMap<String, String>,
1288}
1289
1290impl From<TemplateLinked> for LiteralTemplateLinked {
1291 fn from(i: TemplateLinked) -> Self {
1292 Self {
1293 template_id: i.template_id,
1294 link_id: i.link_id,
1295 args: i
1296 .args
1297 .into_iter()
1298 .map(|(k, v)| (format!("{k}"), v))
1299 .collect(),
1300 }
1301 }
1302}
1303
1304fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1306 for template_linked in load_links_from_file(path)? {
1307 let slot_env = create_slot_env(&template_linked.args)?;
1308 policy_set.link(
1309 PolicyId::new(&template_linked.template_id),
1310 PolicyId::new(&template_linked.link_id),
1311 slot_env,
1312 )?;
1313 }
1314 Ok(())
1315}
1316
1317fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1319 let f = match std::fs::File::open(path) {
1320 Ok(f) => f,
1321 Err(_) => {
1322 return Ok(vec![]);
1324 }
1325 };
1326 if f.metadata()
1327 .into_diagnostic()
1328 .wrap_err("Failed to read metadata")?
1329 .len()
1330 == 0
1331 {
1332 Ok(vec![])
1334 } else {
1335 serde_json::from_reader(f)
1337 .into_diagnostic()
1338 .wrap_err("Deserialization error")
1339 }
1340}
1341
1342fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1344 let mut template_linked = load_links_from_file(path.as_ref())?;
1345 template_linked.push(new_linked);
1346 write_template_linked_file(&template_linked, path.as_ref())
1347}
1348
1349fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1351 let f = OpenOptions::new()
1352 .write(true)
1353 .truncate(true)
1354 .create(true)
1355 .open(path)
1356 .into_diagnostic()?;
1357 serde_json::to_writer(f, linked).into_diagnostic()
1358}
1359
1360pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1361 println!();
1362 let ans = execute_request(
1363 &args.request,
1364 &args.policies,
1365 &args.entities_file,
1366 &args.schema,
1367 args.timing,
1368 );
1369 match ans {
1370 Ok(ans) => {
1371 let status = match ans.decision() {
1372 Decision::Allow => {
1373 println!("ALLOW");
1374 CedarExitCode::Success
1375 }
1376 Decision::Deny => {
1377 println!("DENY");
1378 CedarExitCode::AuthorizeDeny
1379 }
1380 };
1381 if ans.diagnostics().errors().peekable().peek().is_some() {
1382 println!();
1383 for err in ans.diagnostics().errors() {
1384 println!("{err}");
1385 }
1386 }
1387 if args.verbose {
1388 println!();
1389 if ans.diagnostics().reason().peekable().peek().is_none() {
1390 println!("note: no policies applied to this request");
1391 } else {
1392 println!("note: this decision was due to the following policies:");
1393 for reason in ans.diagnostics().reason() {
1394 println!(" {}", reason);
1395 }
1396 println!();
1397 }
1398 }
1399 status
1400 }
1401 Err(errs) => {
1402 for err in errs {
1403 println!("{err:?}");
1404 }
1405 CedarExitCode::Failure
1406 }
1407 }
1408}
1409
1410#[cfg(not(feature = "partial-eval"))]
1411pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1412 {
1413 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1414 return CedarExitCode::Failure;
1415 }
1416}
1417
1418#[cfg(feature = "partial-eval")]
1419pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1420 println!();
1421 let ans = execute_partial_request(
1422 &args.request,
1423 &args.policies,
1424 &args.entities_file,
1425 &args.schema,
1426 args.timing,
1427 );
1428 match ans {
1429 Ok(ans) => match ans.decision() {
1430 Some(Decision::Allow) => {
1431 println!("ALLOW");
1432 CedarExitCode::Success
1433 }
1434 Some(Decision::Deny) => {
1435 println!("DENY");
1436 CedarExitCode::AuthorizeDeny
1437 }
1438 None => {
1439 println!("UNKNOWN");
1440 println!("All policy residuals:");
1441 for p in ans.nontrivial_residuals() {
1442 println!("{p}");
1443 }
1444 CedarExitCode::Unknown
1445 }
1446 },
1447 Err(errs) => {
1448 for err in errs {
1449 println!("{err:?}");
1450 }
1451 CedarExitCode::Failure
1452 }
1453 }
1454}
1455
1456fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1458 match std::fs::OpenOptions::new()
1459 .read(true)
1460 .open(entities_filename.as_ref())
1461 {
1462 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1463 format!(
1464 "failed to parse entities from file {}",
1465 entities_filename.as_ref().display()
1466 )
1467 }),
1468 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1469 format!(
1470 "failed to open entities file {}",
1471 entities_filename.as_ref().display()
1472 )
1473 }),
1474 }
1475}
1476
1477fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
1484 let mut new_ps = PolicySet::new();
1485 let t_iter = ps.templates().map(|t| match t.annotation("id") {
1486 None => Ok(t.clone()),
1487 Some(anno) => anno.parse().map(|a| t.new_id(a)),
1488 });
1489 for t in t_iter {
1490 let template = t.unwrap_or_else(|never| match never {});
1491 new_ps
1492 .add_template(template)
1493 .wrap_err("failed to add template to policy set")?;
1494 }
1495 let p_iter = ps.policies().map(|p| match p.annotation("id") {
1496 None => Ok(p.clone()),
1497 Some(anno) => anno.parse().map(|a| p.new_id(a)),
1498 });
1499 for p in p_iter {
1500 let policy = p.unwrap_or_else(|never| match never {});
1501 new_ps
1502 .add(policy)
1503 .wrap_err("failed to add template to policy set")?;
1504 }
1505 Ok(new_ps)
1506}
1507
1508fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
1510 let mut src_str = String::new();
1511 match filename {
1512 Some(path) => {
1513 src_str = std::fs::read_to_string(path)
1514 .into_diagnostic()
1515 .wrap_err_with(|| {
1516 format!("failed to open {context} file {}", path.as_ref().display())
1517 })?;
1518 }
1519 None => {
1520 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1521 .into_diagnostic()
1522 .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1523 }
1524 };
1525 Ok(src_str)
1526}
1527
1528fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1530 read_from_file_or_stdin(Some(&filename), context)
1531}
1532
1533fn read_cedar_policy_set(
1536 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1537) -> Result<PolicySet> {
1538 let context = "policy set";
1539 let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
1540 let ps = PolicySet::from_str(&ps_str)
1541 .map_err(|err| {
1542 let name = filename.map_or_else(
1543 || "<stdin>".to_owned(),
1544 |n| n.as_ref().display().to_string(),
1545 );
1546 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1547 })
1548 .wrap_err_with(|| format!("failed to parse {context}"))?;
1549 rename_from_id_annotation(&ps)
1550}
1551
1552fn read_json_policy_set(
1555 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1556) -> Result<PolicySet> {
1557 let context = "JSON policy";
1558 let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
1559 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1560 let policy_type = get_json_policy_type(&json)?;
1561
1562 let add_json_source = |report: Report| {
1563 let name = filename.map_or_else(
1564 || "<stdin>".to_owned(),
1565 |n| n.as_ref().display().to_string(),
1566 );
1567 report.with_source_code(NamedSource::new(name, json_source.clone()))
1568 };
1569
1570 match policy_type {
1571 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1572 Ok(policy) => PolicySet::from_policies([policy])
1573 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1574 Err(_) => match Template::from_json(None, json)
1575 .map_err(|err| add_json_source(Report::new(err)))
1576 {
1577 Ok(template) => {
1578 let mut ps = PolicySet::new();
1579 ps.add_template(template)?;
1580 Ok(ps)
1581 }
1582 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1583 },
1584 },
1585 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1586 .map_err(|err| add_json_source(Report::new(err)))
1587 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1588 }
1589}
1590
1591fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1592 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1593 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1594
1595 let json_has_property = |p| json.get(p).is_some();
1596 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1597 let has_any_policy_property = policy_properties.iter().any(json_has_property);
1598
1599 match (has_any_policy_set_property, has_any_policy_property) {
1600 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1601 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1602 (true, _) => Ok(JsonPolicyType::PolicySet),
1603 (_, true) => Ok(JsonPolicyType::SinglePolicy),
1604 }
1605}
1606
1607enum JsonPolicyType {
1608 SinglePolicy,
1609 PolicySet,
1610}
1611
1612fn execute_request(
1614 request: &RequestArgs,
1615 policies: &PoliciesArgs,
1616 entities_filename: impl AsRef<Path>,
1617 schema: &OptionalSchemaArgs,
1618 compute_duration: bool,
1619) -> Result<Response, Vec<Report>> {
1620 let mut errs = vec![];
1621 let policies = match policies.get_policy_set() {
1622 Ok(pset) => pset,
1623 Err(e) => {
1624 errs.push(e);
1625 PolicySet::new()
1626 }
1627 };
1628 let schema = match schema.get_schema() {
1629 Ok(opt) => opt,
1630 Err(e) => {
1631 errs.push(e);
1632 None
1633 }
1634 };
1635 let entities = match load_entities(entities_filename, schema.as_ref()) {
1636 Ok(entities) => entities,
1637 Err(e) => {
1638 errs.push(e);
1639 Entities::empty()
1640 }
1641 };
1642 match request.get_request(schema.as_ref()) {
1643 Ok(request) if errs.is_empty() => {
1644 let authorizer = Authorizer::new();
1645 let auth_start = Instant::now();
1646 let ans = authorizer.is_authorized(&request, &policies, &entities);
1647 let auth_dur = auth_start.elapsed();
1648 if compute_duration {
1649 println!(
1650 "Authorization Time (micro seconds) : {}",
1651 auth_dur.as_micros()
1652 );
1653 }
1654 Ok(ans)
1655 }
1656 Ok(_) => Err(errs),
1657 Err(e) => {
1658 errs.push(e.wrap_err("failed to parse request"));
1659 Err(errs)
1660 }
1661 }
1662}
1663
1664#[cfg(feature = "partial-eval")]
1665fn execute_partial_request(
1666 request: &PartialRequestArgs,
1667 policies: &PoliciesArgs,
1668 entities_filename: impl AsRef<Path>,
1669 schema: &OptionalSchemaArgs,
1670 compute_duration: bool,
1671) -> Result<PartialResponse, Vec<Report>> {
1672 let mut errs = vec![];
1673 let policies = match policies.get_policy_set() {
1674 Ok(pset) => pset,
1675 Err(e) => {
1676 errs.push(e);
1677 PolicySet::new()
1678 }
1679 };
1680 let schema = match schema.get_schema() {
1681 Ok(opt) => opt,
1682 Err(e) => {
1683 errs.push(e);
1684 None
1685 }
1686 };
1687 let entities = match load_entities(entities_filename, schema.as_ref()) {
1688 Ok(entities) => entities,
1689 Err(e) => {
1690 errs.push(e);
1691 Entities::empty()
1692 }
1693 };
1694 match request.get_request(schema.as_ref()) {
1695 Ok(request) if errs.is_empty() => {
1696 let authorizer = Authorizer::new();
1697 let auth_start = Instant::now();
1698 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1699 let auth_dur = auth_start.elapsed();
1700 if compute_duration {
1701 println!(
1702 "Authorization Time (micro seconds) : {}",
1703 auth_dur.as_micros()
1704 );
1705 }
1706 Ok(ans)
1707 }
1708 Ok(_) => Err(errs),
1709 Err(e) => {
1710 errs.push(e.wrap_err("failed to parse request"));
1711 Err(errs)
1712 }
1713 }
1714}