diff --git a/Cargo.lock b/Cargo.lock index 3e3933421..79cf7058d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,9 +298,9 @@ dependencies = [ name = "cedar-policy" version = "4.1.0" dependencies = [ - "cedar-policy-core", + "cedar-policy-core 4.1.0", "cedar-policy-formatter", - "cedar-policy-validator", + "cedar-policy-validator 4.1.0", "cool_asserts", "criterion", "dhat", @@ -330,11 +330,15 @@ version = "4.1.0" dependencies = [ "assert_cmd", "cedar-policy", + "cedar-policy-core 4.1.0 (git+https://github.com/cedar-policy/cedar.git?branch=main)", "cedar-policy-formatter", + "cedar-policy-validator 4.1.0 (git+https://github.com/cedar-policy/cedar.git?branch=main)", "clap", "glob", "miette", "predicates", + "prost", + "prost-build", "rstest", "semver", "serde", @@ -373,11 +377,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cedar-policy-core" +version = "4.1.0" +source = "git+https://github.com/cedar-policy/cedar.git?branch=main#2e60d27a9536ad41824233c06e6fc7a701a404c4" +dependencies = [ + "chrono", + "either", + "itertools 0.13.0", + "lalrpop", + "lalrpop-util", + "lazy_static", + "miette", + "nonempty", + "prost", + "prost-build", + "ref-cast", + "regex", + "rustc_lexer", + "serde", + "serde_json", + "serde_with", + "smol_str", + "stacker", + "thiserror 2.0.3", +] + [[package]] name = "cedar-policy-formatter" version = "4.1.0" dependencies = [ - "cedar-policy-core", + "cedar-policy-core 4.1.0", "insta", "itertools 0.13.0", "lazy_static", @@ -393,7 +423,7 @@ name = "cedar-policy-validator" version = "4.1.0" dependencies = [ "arbitrary", - "cedar-policy-core", + "cedar-policy-core 4.1.0", "cool_asserts", "itertools 0.13.0", "lalrpop", @@ -417,14 +447,38 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cedar-policy-validator" +version = "4.1.0" +source = "git+https://github.com/cedar-policy/cedar.git?branch=main#2e60d27a9536ad41824233c06e6fc7a701a404c4" +dependencies = [ + "cedar-policy-core 4.1.0 (git+https://github.com/cedar-policy/cedar.git?branch=main)", + "itertools 0.13.0", + "lalrpop", + "lalrpop-util", + "lazy_static", + "miette", + "nonempty", + "prost", + "prost-build", + "ref-cast", + "serde", + "serde_json", + "serde_with", + "smol_str", + "stacker", + "thiserror 2.0.3", + "unicode-security", +] + [[package]] name = "cedar-testing" version = "4.1.0" dependencies = [ "assert_cmd", "cedar-policy", - "cedar-policy-core", - "cedar-policy-validator", + "cedar-policy-core 4.1.0", + "cedar-policy-validator 4.1.0", "miette", "serde", "serde_json", @@ -438,9 +492,9 @@ version = "4.1.0" dependencies = [ "cargo-lock", "cedar-policy", - "cedar-policy-core", + "cedar-policy-core 4.1.0", "cedar-policy-formatter", - "cedar-policy-validator", + "cedar-policy-validator 4.1.0", "console_error_panic_hook", "cool_asserts", "itertools 0.13.0", diff --git a/cedar-policy-cli/Cargo.toml b/cedar-policy-cli/Cargo.toml index 3f213c06b..0734840ce 100644 --- a/cedar-policy-cli/Cargo.toml +++ b/cedar-policy-cli/Cargo.toml @@ -13,12 +13,18 @@ repository.workspace = true [dependencies] cedar-policy = { version = "=4.1.0", path = "../cedar-policy" } cedar-policy-formatter = { version = "=4.1.0", path = "../cedar-policy-formatter" } +cedar-policy-core = { package= "cedar-policy-core", git = "https://github.com/cedar-policy/cedar.git", branch = "main", version = "=4.1.0"} +cedar-policy-validator = { package= "cedar-policy-validator", git = "https://github.com/cedar-policy/cedar.git", branch = "main", version = "=4.1.0"} clap = { version = "4", features = ["derive", "env"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" miette = { version = "7.1.0", features = ["fancy"] } thiserror = "2.0" semver = "1.0.23" +prost = {version = "0.13", optional = true} + +[build-dependencies] +prost-build = {version = "0.13", optional = true} [features] default = [] @@ -26,6 +32,7 @@ experimental = ["permissive-validate", "partial-validate", "partial-eval"] permissive-validate = ["cedar-policy/permissive-validate"] partial-validate = ["cedar-policy/partial-validate"] partial-eval = ["cedar-policy/partial-eval"] +protobufs = ["dep:prost", "dep:prost-build", "cedar-policy/protobufs", "cedar-policy-core/protobufs", "cedar-policy-validator/protobufs"] [dev-dependencies] assert_cmd = "2.0" diff --git a/cedar-policy-cli/build.rs b/cedar-policy-cli/build.rs new file mode 100644 index 000000000..5cf129be2 --- /dev/null +++ b/cedar-policy-cli/build.rs @@ -0,0 +1,20 @@ +fn main() { + #[cfg(feature = "protobufs")] + generate_schemas(); +} + +/// Reads protobuf schema files (.proto) and generates Rust modules +#[cfg(feature = "protobufs")] +fn generate_schemas() { + let mut config = prost_build::Config::new(); + config.extern_path(".cedar_policy_core", "crate::cedar_policy_core::ast::proto"); + config.extern_path( + ".cedar_policy_validator", + "crate::cedar_policy_validator::proto", + ); + // PANIC SAFETY: compile-time unwrap + #[allow(clippy::unwrap_used)] + config + .compile_protos(&["protobuf_schema/CLI.proto"], &["protobuf_schema"]) + .unwrap(); +} diff --git a/cedar-policy-cli/protobuf_schema/AST.proto b/cedar-policy-cli/protobuf_schema/AST.proto new file mode 100644 index 000000000..c4f8361d9 --- /dev/null +++ b/cedar-policy-cli/protobuf_schema/AST.proto @@ -0,0 +1,333 @@ +// +// Copyright Cedar Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; +package cedar_policy_core; + +message Request { + EntityUidEntry principal = 1; + EntityUidEntry action = 2; + EntityUidEntry resource = 3; + Context context = 4; +} + +message LiteralPolicySet { + // Key is PolicyID as a string + map templates = 1; + map links = 2; +} + +enum Mode { + Concrete = 0; + Partial = 1; +} + +message Entities { + repeated Entity entities = 1; + Mode mode = 2; +} + +message Context { + Expr context = 1; +} + +// BEGIN REQUEST MESSAGES + +message EntityUidEntry { + EntityUid euid = 1; + Loc loc = 2; +} + +message EntityUid { + EntityType ty = 1; + string eid = 2; + Loc loc = 3; +} + +message EntityType { + Name name = 1; +} + +// alias Id = string +message Name { + string id = 1; + repeated string path = 2; + Loc loc = 3; +} + +message Loc { + uint32 offset = 1; + uint32 length = 2; + string src = 3; +} + + +// END REQUEST MESSAGES + + +// BEGIN POLICYSET MESSAGES + +message LiteralPolicy { + string template_id = 1; + string link_id = 2; + bool link_id_specified = 3; + // map is not allowed since keys in map + // fields cannot be enum types + // map values = 4; + EntityUid principal_euid = 4; + EntityUid resource_euid = 5; +} + +message Annotation { + string val = 1; + Loc loc = 2; +} + +enum Effect { + Forbid = 0; + Permit = 1; +} + +message TemplateBody { + string id = 1; + Loc loc = 2; + // alias AnyId = string + // alias Annotations = map + map annotations = 3; + Effect effect = 4; + PrincipalConstraint principal_constraint = 5; + ActionConstraint action_constraint = 6; + ResourceConstraint resource_constraint = 7; + Expr non_scope_constraints = 8; +} + +message PrincipalConstraint { + PrincipalOrResourceConstraint constraint = 1; +} + +message ResourceConstraint { + PrincipalOrResourceConstraint constraint = 1; +} + +message EntityReference { + oneof data { + Ty ty = 1; + EntityUid euid = 2; + } + + // Zero-Arity constructors + enum Ty { + Slot = 0; + } +} + +message PrincipalOrResourceConstraint { + oneof data { + Ty ty = 1; + InMessage in = 2; + EqMessage eq = 3; + IsMessage is = 4; + IsInMessage isIn = 5; + } + + // Zero-arity constructors + enum Ty { + Any = 0; + } + + message InMessage { + EntityReference er = 1; + } + message EqMessage { + EntityReference er = 1; + } + message IsMessage { + EntityType et = 1; + } + message IsInMessage { + EntityReference er = 1; + EntityType et = 2; + } +} + +enum SlotId { + Principal = 0; + Resource = 1; +} + +message ActionConstraint { + oneof data { + Ty ty = 1; + InMessage in = 2; + EqMessage eq = 3; + } + + enum Ty { + Any = 0; + } + message InMessage { + repeated EntityUid euids = 1; + } + message EqMessage { + EntityUid euid = 1; + } +} + +message Expr { + ExprKind expr_kind = 1; + Loc source_loc = 2; + + message ExprKind { + oneof data { + Literal lit = 1; + Var var = 2; + SlotId slot = 3; + If if = 4; + And and = 5; + Or or = 6; + UnaryApp uApp = 7; + BinaryApp bApp = 8; + ExtensionFunctionApp extApp = 9; + GetAttr getAttr = 10; + HasAttr hasAttr = 11; + Like like = 12; + Is is = 13; + Set set = 14; + Record record = 15; + } + } + message Literal { + oneof lit { + bool b = 1; + int64 i = 2; + string s = 3; + EntityUid euid = 4; + } + } + + enum Var { + Principal = 0; + Action = 1; + Resource = 2; + CONTEXT = 3; + } + + message If { + Expr test_expr = 1; + Expr then_expr = 2; + Expr else_expr = 3; + } + + message And { + Expr left = 1; + Expr right = 2; + } + + message Or { + Expr left = 1; + Expr right = 2; + } + + message UnaryApp { + Op op = 1; + Expr expr = 2; + + enum Op { + Not = 0; + Neg = 1; + } + } + + message BinaryApp { + Op op = 1; + Expr left = 2; + Expr right = 3; + + enum Op { + Eq = 0; + Less = 1; + LessEq = 2; + Add = 3; + Sub = 4; + Mul = 5; + In = 6; + Contains = 7; + ContainsAll = 8; + ContainsAny = 9; + GetTag = 10; + HasTag = 11; + } + } + + message ExtensionFunctionApp { + Name fn_name = 1; + repeated Expr args = 2; + } + + message GetAttr { + Expr expr = 1; + string attr = 2; + } + + message HasAttr { + Expr expr = 1; + string attr = 2; + } + + message Like { + Expr expr = 1; + repeated PatternElem pattern = 2; + + message PatternElem { + oneof data { + Ty ty = 1; + string c = 2; + } + + // Zero-arity constructors + enum Ty { + Wildcard = 0; + } + } + } + + message Is { + Expr expr = 1; + EntityType entity_type = 2; + } + + message Set { + repeated Expr elements = 1; + } + + message Record { + map items = 1; + } +} + +// END POLICYSET MESSAGES + + +// ENTER ENTITITES MESSAGES + +message Entity { + EntityUid uid = 1; + map attrs = 2; + repeated EntityUid ancestors = 3; + map tags = 4; +} + +// END ENTITITES MESSAGES diff --git a/cedar-policy-cli/protobuf_schema/CLI.proto b/cedar-policy-cli/protobuf_schema/CLI.proto new file mode 100644 index 000000000..f055e15a4 --- /dev/null +++ b/cedar-policy-cli/protobuf_schema/CLI.proto @@ -0,0 +1,35 @@ +// +// Copyright Cedar Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; +package cedar_internal_cli; + +import "AST.proto"; +import "Validator.proto"; + +message ValidationRequestMsg { + cedar_policy_validator.ValidatorSchema schema = 1; + cedar_policy_core.LiteralPolicySet policies = 2; + cedar_policy_validator.ValidationMode mode = 3; +} + +message EquivRequestMsg { + cedar_policy_validator.ValidatorSchema schema = 1; + cedar_policy_core.LiteralPolicySet old_policies = 2; + cedar_policy_core.LiteralPolicySet new_policies = 3; +} + + diff --git a/cedar-policy-cli/protobuf_schema/Validator.proto b/cedar-policy-cli/protobuf_schema/Validator.proto new file mode 100644 index 000000000..c205ef7ca --- /dev/null +++ b/cedar-policy-cli/protobuf_schema/Validator.proto @@ -0,0 +1,127 @@ +// +// Copyright Cedar Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; +package cedar_policy_validator; + +import "AST.proto"; + +message ValidatorSchema { + repeated EntityTypeWithTypesMap entity_types = 1; + repeated EntityUidWithActionIdsMap action_ids = 2; +} + +// Workaround since messages can't be dictionary keys +message EntityTypeWithTypesMap { + cedar_policy_core.EntityType key = 1; + ValidatorEntityType value = 2; +} + +message EntityUidWithActionIdsMap { + cedar_policy_core.EntityUid key = 1; + ValidatorActionId value = 2; +} + +message ValidatorEntityType { + cedar_policy_core.EntityType name = 1; + repeated cedar_policy_core.EntityType descendants = 2; + Attributes attributes = 3; + OpenTag open_attributes = 4; + Tag tags = 5; +} + +message ValidatorActionId { + cedar_policy_core.EntityUid name = 1; + ValidatorApplySpec applies_to = 2; + repeated cedar_policy_core.EntityUid descendants = 3; + Type context = 4; + Attributes attribute_types = 5; + // Deserialize Expr as Value + map attributes = 6; +} + +message ValidatorApplySpec { + repeated cedar_policy_core.EntityType principal_apply_spec = 1; + repeated cedar_policy_core.EntityType resource_apply_spec = 2; +} + +message Type { + oneof data { + Ty ty = 1; + Type set_type = 2; + EntityRecordKind entityOrRecord = 3; + cedar_policy_core.Name name = 4; + } + + enum Ty { + Never = 0; + True = 1; + False = 2; + EmptySetType = 3; + Bool = 4; + String = 5; + Long = 6; + } +} + +message EntityRecordKind { + oneof data { + Ty ty = 1; + Record record = 2; + Entity entity = 3; + ActionEntity actionEntity = 4; + } + + enum Ty { + AnyEntity = 0; // AnyEntity + } + message Record { + Attributes attrs = 1; + OpenTag open_attributes = 2; + } + message Entity { + cedar_policy_core.EntityType e = 1; + } + message ActionEntity { + cedar_policy_core.EntityType name = 1; + Attributes attrs = 2; + } +} + +enum OpenTag { + OpenAttributes = 0; + ClosedAttributes = 1; +} + +message Attributes { + map attrs = 1; +} + +message AttributeType { + Type attr_type = 1; + bool is_required = 2; +} + +message Tag { + oneof optional_type { + Type type = 1; + } +} + +enum ValidationMode { + Strict = 0; + Permissive = 1; +} diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs index 43acd3feb..5c8e2a01c 100644 --- a/cedar-policy-cli/src/lib.rs +++ b/cedar-policy-cli/src/lib.rs @@ -36,6 +36,18 @@ use std::{ use cedar_policy::*; use cedar_policy_formatter::{policies_str_to_pretty, Config}; +// Needed for the generated code to find `crate::cedar_policy_...` +#[cfg(feature = "protobufs")] +use cedar_policy_core; +#[cfg(feature = "protobufs")] +use cedar_policy_validator; + +#[cfg(feature = "protobufs")] +pub mod proto { + #![allow(missing_docs)] + include!(concat!(env!("OUT_DIR"), "/cedar_internal_cli.rs")); +} + /// Basic Cedar CLI for evaluating authorization queries #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] // Pull from `Cargo.toml` @@ -106,6 +118,17 @@ pub enum Commands { New(NewArgs), /// Partially evaluate an authorization request PartiallyAuthorize(PartiallyAuthorizeArgs), + /// Ouput a JSON file for consumption by Lean + #[command(subcommand)] + WriteDRTJson(serialization::AnalysisCommands), + /// Output a protobuf binary file for consumption by Lean + #[cfg(feature = "protobufs")] + #[command(subcommand)] + WriteDRTProto(serialization::AnalysisCommands), + /// Output a protobuf binary file for consumption by Lean + #[cfg(feature = "protobufs")] + #[command(subcommand)] + WriteDRTProtoFromJSON(serialization::AnalyzeCommandsFromJson), /// Print Cedar language version LanguageVersion, } @@ -1565,3 +1588,329 @@ fn execute_partial_request( } } } + +pub mod serialization { + use cedar_policy_core::{ast::PolicySet, extensions::Extensions, parser::parse_policyset}; + use cedar_policy_validator::CedarSchemaError; + use clap::{Args, Subcommand}; + use serde::Serialize; + use std::path::{Path, PathBuf}; + use thiserror::Error; + + use crate::CedarExitCode; + + /// Captures all possible errors in CLI operations in the `serialization` module + #[derive(Debug, Error)] + pub enum CliError { + /// Error opening or reading a file + #[error("failed to open or read {path}: {err}")] + FileError { + /// Path of the file we failed to open or read + path: PathBuf, + /// Error we encountered + err: std::io::Error, + }, + /// Error when loading the schema + #[error("failed to load cedar schema: {0}")] + CedarSchemaError(#[from] cedar_policy_validator::CedarSchemaError), + /// Error when parsing a policy + #[error("failed to parse policy in {path}: {err}")] + PolicyParse { + /// Path of the policy we failed to parse + path: PathBuf, + /// Error we encountered + err: cedar_policy_core::parser::err::ParseErrors, + }, + /// Failed to find a policy with the specified id + #[error("failed to find a policy with id: {id}")] + PolicyNotFound { + /// Policy id we failed to find + id: cedar_policy_core::ast::PolicyID, + }, + } + + type Result = std::result::Result; + + #[derive(Args, Debug)] + pub struct AnalyzeCommandsFromJsonArgs { + #[arg(short, long = "json", value_name = "String")] + pub data: String, + #[arg(short, long = "output", value_name = "FILE")] + pub output_path: PathBuf, + } + + #[derive(Args, Debug)] + pub struct EquivalenceArgs { + /// File containing the schema + #[arg(short, long = "schema", value_name = "FILE")] + pub schema_file: PathBuf, + /// File containing the policy to analyze + #[arg(short, long = "old_policies", value_name = "FILE")] + pub old_policies_file: PathBuf, + #[arg(short, long = "new_policies", value_name = "FILE")] + pub new_policies_file: PathBuf, + } + + #[derive(Subcommand, Debug)] + pub enum AnalysisCommands { + /// Equivalence check + Equivalence(EquivalenceArgs), + } + + #[derive(Subcommand, Debug)] + pub enum AnalyzeCommandsFromJson { + /// Equivalence check + Equivalence(AnalyzeCommandsFromJsonArgs), + } + + fn read_schema_from_file( + path: impl AsRef + std::marker::Copy, + ) -> Result { + let file = std::fs::File::open(path).map_err(|err| CliError::FileError { + path: path.as_ref().into(), + err, + })?; + match cedar_policy_validator::ValidatorSchema::from_json_file( + file, + Extensions::all_available(), + ) { + Ok(res) => Ok(res), + Err(e) => Err(CliError::CedarSchemaError(CedarSchemaError::Schema(e))), + } + } + + fn read_policies_from_file(path: impl AsRef + std::marker::Copy) -> Result { + let text = std::fs::read_to_string(path).map_err(|err| CliError::FileError { + path: path.as_ref().into(), + err, + })?; + parse_policyset(&text).map_err(|err| CliError::PolicyParse { + path: path.as_ref().into(), + err, + }) + } + + #[derive(Debug, Serialize)] + pub struct EquivRequest<'a> { + pub schema: &'a cedar_policy_validator::ValidatorSchema, + pub old_policies: &'a PolicySet, + pub new_policies: &'a PolicySet, + } + + pub fn write_drt_json_for_equivalence(args: EquivalenceArgs) -> CedarExitCode { + let schema = &read_schema_from_file(&args.schema_file); + let old_policies = &read_policies_from_file(&args.old_policies_file); + let new_policies = &read_policies_from_file(&args.new_policies_file); + + match (&schema, &old_policies, &new_policies) { + (Ok(schema), Ok(old_policies), Ok(new_policies)) => { + match serde_json::to_string(&EquivRequest { + schema, + old_policies, + new_policies, + }) { + Ok(s) => { + println!("{s}"); + CedarExitCode::Success + } + Err(e) => { + eprintln!("{e}"); + CedarExitCode::Failure + } + } + } + (_, _, _) => { + if let Some(e) = schema.as_ref().err() { + eprintln!("{e}"); + } + if let Some(e) = old_policies.as_ref().err() { + eprintln!("{e}"); + } + if let Some(e) = new_policies.as_ref().err() { + eprintln!("{e}"); + } + CedarExitCode::Failure + } + } + } + + pub fn write_drt_json(acmd: AnalysisCommands) -> CedarExitCode { + match acmd { + AnalysisCommands::Equivalence(args) => write_drt_json_for_equivalence(args), + } + } + + #[cfg(feature = "protobufs")] + pub mod protobuf { + // PANIC SAFETY experimental feature + #![allow(clippy::unwrap_used)] + // PANIC SAFETY experimental feature + #![allow(clippy::expect_used)] + + use std::fs::File; + use std::io::Write; + use std::path::PathBuf; + + use super::{ + read_policies_from_file, AnalysisCommands, AnalyzeCommandsFromJson, + AnalyzeCommandsFromJsonArgs, EquivRequest, + }; + use super::{EquivalenceArgs, Result}; + use crate::serialization::read_schema_from_file; + use crate::{proto, CedarExitCode}; + use cedar_policy_core::ast::PolicySet; + use cedar_policy_core::parser::parse_policyset; + use prost::Message; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize)] + pub struct ValidationRequest<'a> { + pub schema: &'a cedar_policy_validator::ValidatorSchema, + pub policies: &'a PolicySet, + pub mode: cedar_policy_validator::ValidationMode, + } + + impl From> for proto::ValidationRequestMsg { + fn from(v: ValidationRequest<'_>) -> Self { + Self { + schema: Some(cedar_policy_validator::proto::ValidatorSchema::from( + v.schema, + )), + policies: Some(cedar_policy_core::ast::proto::LiteralPolicySet::from( + v.policies, + )), + mode: cedar_policy_validator::proto::ValidationMode::from(&v.mode).into(), + } + } + } + + impl From> for proto::EquivRequestMsg { + fn from(v: EquivRequest<'_>) -> Self { + Self { + schema: Some(cedar_policy_validator::proto::ValidatorSchema::from( + v.schema, + )), + old_policies: Some(cedar_policy_core::ast::proto::LiteralPolicySet::from( + v.old_policies, + )), + new_policies: Some(cedar_policy_core::ast::proto::LiteralPolicySet::from( + v.new_policies, + )), + } + } + } + + pub fn read_equivalence_drt_from_files( + args: EquivalenceArgs, + ) -> Result { + let schema = &read_schema_from_file(&args.schema_file)?; + let old_policies = &read_policies_from_file(&args.old_policies_file)?; + let new_policies = &read_policies_from_file(&args.new_policies_file)?; + + let equiv_request = EquivRequest { + schema, + old_policies, + new_policies, + }; + let equiv_request_proto = proto::EquivRequestMsg::from(equiv_request); + Ok(equiv_request_proto) + } + + pub fn write_drt_proto_for_equivalence_from_files(args: EquivalenceArgs) -> Result<()> { + let equiv_request_proto: proto::EquivRequestMsg = + read_equivalence_drt_from_files(args)?; + write_drt_proto_for_equivalence(equiv_request_proto, "equiv_request.binpb".into()) + } + + #[derive(Debug, Deserialize)] + struct ComparisonRequest { + schema: String, + old_policy_set: String, + new_policy_set: String, + } + + pub fn read_equivalence_drt_from_json( + args: AnalyzeCommandsFromJsonArgs, + ) -> Result { + use std::str::FromStr; + + let comparison_request: ComparisonRequest = + serde_json::from_str(args.data.as_ref()).expect("Failed to parse"); + + let schema = + cedar_policy_validator::ValidatorSchema::from_str(&comparison_request.schema) + .expect("Failed to deserialize schema"); + + let old_policies = parse_policyset(&comparison_request.old_policy_set).unwrap(); + + let new_policies = parse_policyset(&comparison_request.new_policy_set).unwrap(); + + Ok(proto::EquivRequestMsg { + schema: Some(cedar_policy_validator::proto::ValidatorSchema::from( + &schema, + )), + old_policies: Some(cedar_policy_core::ast::proto::LiteralPolicySet::from( + &old_policies, + )), + new_policies: Some(cedar_policy_core::ast::proto::LiteralPolicySet::from( + &new_policies, + )), + }) + } + + pub fn write_drt_proto_for_equivalence_from_json( + args: AnalyzeCommandsFromJsonArgs, + ) -> Result<()> { + let output_path = args.output_path.clone(); + let equiv_request_proto: proto::EquivRequestMsg = read_equivalence_drt_from_json(args)?; + write_drt_proto_for_equivalence(equiv_request_proto, output_path) + } + + pub fn write_drt_proto_for_equivalence( + equiv_request_proto: proto::EquivRequestMsg, + output_location: PathBuf, + ) -> Result<()> { + let mut buf: Vec = vec![]; + buf.reserve(equiv_request_proto.encoded_len()); + equiv_request_proto + .encode(&mut buf) + .expect("Serialization failed"); + + let mut file = File::create(output_location).unwrap(); + // Write a slice of bytes to the file + file.write_all(&buf).unwrap(); + + Ok(()) + } + + pub fn write_drt_proto(acmd: AnalysisCommands) -> CedarExitCode { + let res = match acmd { + AnalysisCommands::Equivalence(args) => { + write_drt_proto_for_equivalence_from_files(args) + } + }; + match res { + Ok(()) => CedarExitCode::Success, + Err(e) => { + eprintln!("{e}"); + CedarExitCode::Failure + } + } + } + + pub fn write_drt_proto_from_json(acmd: AnalyzeCommandsFromJson) -> CedarExitCode { + let res = match acmd { + AnalyzeCommandsFromJson::Equivalence(args) => { + write_drt_proto_for_equivalence_from_json(args) + } + }; + match res { + Ok(()) => CedarExitCode::Success, + Err(e) => { + eprintln!("{e}"); + CedarExitCode::Failure + } + } + } + } +} diff --git a/cedar-policy-cli/src/main.rs b/cedar-policy-cli/src/main.rs index 069b9fac1..e9011f780 100644 --- a/cedar-policy-cli/src/main.rs +++ b/cedar-policy-cli/src/main.rs @@ -21,8 +21,13 @@ use miette::ErrorHook; use cedar_policy_cli::{ authorize, check_parse, evaluate, format_policies, language_version, link, new, - partial_authorize, translate_policy, translate_schema, validate, visualize, CedarExitCode, Cli, - Commands, ErrorFormat, + partial_authorize, serialization::write_drt_json, translate_policy, translate_schema, validate, + visualize, CedarExitCode, Cli, Commands, ErrorFormat, +}; + +#[cfg(feature = "protobufs")] +use cedar_policy_cli::{ + serialization::protobuf::write_drt_proto, serialization::protobuf::write_drt_proto_from_json, }; fn main() -> CedarExitCode { @@ -53,6 +58,72 @@ fn main() -> CedarExitCode { Commands::TranslateSchema(args) => translate_schema(&args), Commands::New(args) => new(&args), Commands::PartiallyAuthorize(args) => partial_authorize(&args), + Commands::WriteDRTJson(acmd) => write_drt_json(acmd), + #[cfg(feature = "protobufs")] + Commands::WriteDRTProto(acmd) => write_drt_proto(acmd), + #[cfg(feature = "protobufs")] + Commands::WriteDRTProtoFromJSON(acmd) => write_drt_proto_from_json(acmd), Commands::LanguageVersion => language_version(), } } + +#[cfg(test)] +mod test { + use cedar_policy_cli::serialization::AnalysisCommands; + use cedar_policy_cli::serialization::EquivalenceArgs; + use std::path::Path; + use std::path::PathBuf; + + #[test] + fn test_json_serialize() { + let test_data_root = PathBuf::from(r"../sample-data/sandbox_b"); + let schema_file = Path::new(&test_data_root).join("schema.cedarschema"); + let old_policies_file = Path::new(&test_data_root).join("policies_4.cedar"); + let new_policies_file = old_policies_file.clone(); + + let acmd = AnalysisCommands::Equivalence(EquivalenceArgs { + schema_file, + old_policies_file, + new_policies_file, + }); + super::write_drt_json(acmd); + } + + #[cfg(feature = "protobufs")] + #[test] + fn test_proto_serialize() { + let test_data_root = PathBuf::from(r"../sample-data/sandbox_b"); + let mut schema_file = test_data_root.clone(); + schema_file.push("schema.cedarschema"); + let mut old_policies_file = test_data_root.clone(); + old_policies_file.push("policies_4.cedar"); + let new_policies_file = old_policies_file.clone(); + + let acmd = AnalysisCommands::Equivalence(EquivalenceArgs { + schema_file, + old_policies_file, + new_policies_file, + }); + super::write_drt_proto(acmd); + } + + #[cfg(feature = "protobufs")] + #[test] + fn test_proto_serialize_from_json() { + use cedar_policy_cli::serialization::AnalyzeCommandsFromJson; + let data = r#" + { + "schema":"entity Team, UserGroup in [UserGroup];\r\nentity Issue = {\r\n \"repo\": Repository,\r\n \"reporter\": User,\r\n};\r\nentity Org = {\r\n \"members\": UserGroup,\r\n \"owners\": UserGroup,\r\n};\r\nentity Repository = {\r\n \"admins\": UserGroup,\r\n \"maintainers\": UserGroup,\r\n \"readers\": UserGroup,\r\n \"triagers\": UserGroup,\r\n \"writers\": UserGroup,\r\n};\r\nentity User in [UserGroup, Team] = {\r\n \"is_intern\": Bool,\r\n};\r\nentity File = {\r\n \"filename\": String,\r\n \"owner\": User,\r\n \"private\": Bool,\r\n};\r\n\r\naction push, pull, fork appliesTo {\r\n principal: [User],\r\n resource: [Repository]\r\n};\r\naction assign_issue, delete_issue, edit_issue appliesTo {\r\n principal: [User],\r\n resource: [Issue]\r\n};\r\naction add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {\r\n principal: [User],\r\n resource: [Repository]\r\n};\r\naction view, comment appliesTo {\r\n principal: [User],\r\n resource: [File]\r\n};", + "old_policy_set": "permit(principal, action in [Action::\"view\", Action::\"comment\"], resource)\r\n when {\r\n principal == resource.owner ||\r\n ((resource.filename like \"*.png\" ||\r\n resource.filename like \"*.jpg\") && !resource.private)\r\n };\r\n", + "new_policy_set": "permit(principal, action in [Action::\"view\", Action::\"comment\"], resource)\r\n when {\r\n principal == resource.owner ||\r\n ((resource.filename like \"*.png\" ||\r\n resource.filename like \"*.jpg\") && !resource.private)\r\n };\r\n", + "assumptions": "" + } + "#.to_string(); + let output_path = PathBuf::from("/tmp/tmp.binpb"); + + let acmd = AnalyzeCommandsFromJson::Equivalence( + cedar_policy_cli::serialization::AnalyzeCommandsFromJsonArgs { data, output_path }, + ); + super::write_drt_proto_from_json(acmd); + } +} diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 7637dc0c2..43b2757a4 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -16,6 +16,7 @@ Cedar Language Version: TBD ### Added - Added protobuf schemas and (de)serialization code using on `prost` crate behind the experimental `protobufs` flag. +- Added protobuf and JSON generation code to `cedar-policy-cli`. - Added a new get helper method to Context that allows easy extraction of generic values from the context by key. This method simplifies the common use case of retrieving values from Context objects. ## [4.2.2] - 2024-11-11