diff --git a/api.go b/api.go index 762abed..e16a4d3 100644 --- a/api.go +++ b/api.go @@ -12,11 +12,13 @@ const ( isAuthorizedString function = "is_authorized_string" isAuthorizedJSON function = "is_authorized_json" isAuthorizedPartial function = "is_authorized_partial" + initialize function = "initialize" ) // exportFuncs returns a map of exported functions from the wasm module. func exportFuncs(module api.Module) map[string]api.Function { exportedFuncs := make(map[string]api.Function) + exportedFuncs[string(initialize)] = module.ExportedFunction(string(initialize)) exportedFuncs[string(isAuthorizedString)] = module.ExportedFunction(string(isAuthorizedString)) exportedFuncs[string(isAuthorizedJSON)] = module.ExportedFunction(string(isAuthorizedJSON)) exportedFuncs[string(isAuthorizedPartial)] = module.ExportedFunction(string(isAuthorizedPartial)) diff --git a/engine_test.go b/engine_test.go index 4cd0444..9f1ede6 100644 --- a/engine_test.go +++ b/engine_test.go @@ -2,6 +2,7 @@ package cedar import ( "context" + "encoding/json" "testing" ) @@ -134,7 +135,7 @@ func evalJSONMustReturnDeny(t *testing.T, engine *CedarEngine, principal, action } } -func TestCedarEngine_IsAuthorizedPartial(t *testing.T) { +func TestCedarEngine_IsAuthorizedPartialTemplate(t *testing.T) { policy := ` permit( principal == User::"alice", @@ -155,36 +156,150 @@ func TestCedarEngine_IsAuthorizedPartial(t *testing.T) { if err != nil { t.Fatal(err) } - t.Run("is authorized must return allow", func(t *testing.T) { - isAuthorizedPartialMustReturnAllow(t, engine, "User::\"alice\"", "Action::\"update\"", "Photo::\"VacationPhoto94.jpg\"") + t.Run("is authorized partial must return allow", func(t *testing.T) { + isAuthorizedPartialMustReturnAllow(t, engine, "User::\"alice\"", "Action::\"update\"", + "Photo::\"VacationPhoto94.jpg\"", "{}") }) - t.Run("is authorized must return deny", func(t *testing.T) { - isAuthorizedPartialMustReturnDeny(t, engine, "User::\"alice\"", "Action::\"update\"", "Photo::\"VacationPhoto95.jpg\"") + t.Run("is authorized partial must return deny", func(t *testing.T) { + isAuthorizedPartialMustReturnDeny(t, engine, "User::\"alice\"", "Action::\"update\"", + "Photo::\"VacationPhoto95.jpg\"", "{}") + }) + t.Run("is authorized partial with missing principal must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "", "Action::\"update\"", + "Photo::\"VacationPhoto95.jpg\"", "{}") + }) + t.Run("is authorized partial with missing action must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "User::\"alice\"", "", + "Photo::\"VacationPhoto95.jpg\"", "{}") + }) + t.Run("is authorized partial with missing resource must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "User::\"alice\"", "Action::\"update\"", + "", "{}") }) } -func isAuthorizedPartialMustReturnAllow(t *testing.T, engine *CedarEngine, principal, action, resource string) { - res, err := engine.IsAuthorizedPartial(context.Background(), EvalRequest{ +func TestCedarEngine_IsAuthorizedPartialCondition(t *testing.T) { + policy := ` + permit(principal, action, resource) + when { + principal == User::"alice" && + action == Action::"update" && + resource == Photo::"VacationPhoto94.jpg" && + context.test == "foo" + }; + ` + engine, err := NewCedarEngine(context.Background()) + if err != nil { + t.Fatal(err) + } + defer engine.Close(context.Background()) + err = engine.SetEntitiesFromJson(context.Background(), "[]") + if err != nil { + t.Fatal(err) + } + err = engine.SetPolicies(context.Background(), policy) + if err != nil { + t.Fatal(err) + } + t.Run("is authorized partial must return allow", func(t *testing.T) { + isAuthorizedPartialMustReturnAllow(t, engine, "User::\"alice\"", "Action::\"update\"", + "Photo::\"VacationPhoto94.jpg\"", "{\"test\":\"foo\"}") + }) + t.Run("is authorized partial must return deny", func(t *testing.T) { + isAuthorizedPartialMustReturnDeny(t, engine, "User::\"alice\"", "Action::\"update\"", + "Photo::\"VacationPhoto95.jpg\"", "{\"test\":\"foo\"}") + }) + t.Run("is authorized partial with no principal must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "f", "Action::\"update\"", + "Photo::\"VacationPhoto95.jpg\"", "{\"test\":\"foo\"}") + }) + t.Run("is authorized partial with no action must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "User::\"alice\"", "", + "", "{\"test\":\"foo\"}") + }) + t.Run("is authorized partial with no resource must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "User::\"alice\"", "Action::\"update\"", + "", "{\"test\":\"foo\"}") + }) + t.Run("is authorized partial with missing context attribute must return residual", func(t *testing.T) { + isAuthorizedPartialMustReturnResidual(t, engine, "User::\"alice\"", "Action::\"update\"", + "", "{}") + }) +} + +func isAuthorizedPartialMustReturnAllow(t *testing.T, engine *CedarEngine, principal, action, resource, jsonContext string) { + resJson, err := engine.IsAuthorizedPartial(context.Background(), EvalRequest{ Principal: principal, Action: action, Resource: resource, - Context: "{}", + Context: jsonContext, + }) + if err != nil { + t.Fatal(err) + } + + var res = EvalResponse{} + err2 := json.Unmarshal([]byte(resJson), &res) + if err2 != nil { + t.Fatal(err2) + } + + if res.Decision != "Allow" { + t.Fatal("expected Allow") + } + if res.Diagnostics.Reason[0] != "policy0" { // First policy as it is the only one. Cedar engine fixes the policy name to policy if not provided. + t.Fatal("expected policy0 to be the reason for the decision") + } + if len(res.Diagnostics.Errors) != 0 { + t.Fatal("expected no errors") + } +} + +func isAuthorizedPartialMustReturnDeny(t *testing.T, engine *CedarEngine, principal, action, resource, jsonContext string) { + resJson, err := engine.IsAuthorizedPartial(context.Background(), EvalRequest{ + Principal: principal, + Action: action, + Resource: resource, + Context: jsonContext, }) if err != nil { t.Fatal(err) } - println(res) + + var res = EvalResponse{} + err2 := json.Unmarshal([]byte(resJson), &res) + if err2 != nil { + t.Fatal(err2) + } + + if res.Decision != "Deny" { + t.Fatal("expected Deny") + } + if len(res.Diagnostics.Reason) != 0 { + t.Fatal("expected no reason for the decision") + } + if len(res.Diagnostics.Errors) != 0 { + t.Fatal("expected no errors") + } } -func isAuthorizedPartialMustReturnDeny(t *testing.T, engine *CedarEngine, principal, action, resource string) { - res, err := engine.IsAuthorizedPartial(context.Background(), EvalRequest{ +func isAuthorizedPartialMustReturnResidual(t *testing.T, engine *CedarEngine, principal, action, resource, jsonContext string) { + resJson, err := engine.IsAuthorizedPartial(context.Background(), EvalRequest{ Principal: principal, Action: action, Resource: resource, - Context: "{}", + Context: jsonContext, }) if err != nil { t.Fatal(err) } - println(res) + + // TODO: need a new struct for partial results + var res = EvalResponse{} + err2 := json.Unmarshal([]byte(resJson), &res) + if err2 != nil { + t.Fatal(err2, " -- ", resJson) + } + + println("resJson = {}", resJson) } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 90cc71c..58adcee 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" version = "1.0.0" [dependencies] -cedar-policy = { version = "2.0.0" } +cedar-policy = { version = "2.4", features = [ "partial-eval" ] } wee_alloc = "0.4.5" once_cell = "1.17.1" serde_json = "1.0.96" +web-sys = "0.3.65" [lib] crate_type = ["cdylib"] diff --git a/lib/src/interface.rs b/lib/src/interface.rs index 9d9d651..8c07e7f 100644 --- a/lib/src/interface.rs +++ b/lib/src/interface.rs @@ -56,14 +56,22 @@ impl CedarEngine { resource: &str, context: &str, ) -> Result> { - let principal = EntityUid::from_str(entity)?; - let action = EntityUid::from_str(action)?; - let resource = EntityUid::from_str(resource)?; - let context = Context::from_json_str(context, None)?; - let query = Request::new(Some(principal), Some(action), Some(resource), context); + let mut builder = Request::builder(); + if !entity.is_empty() { builder = builder.principal(Some(EntityUid::from_str(entity)?)); } + if !action.is_empty() { builder = builder.action(Some(EntityUid::from_str(action)?)); } + if !resource.is_empty() { builder = builder.resource(Some(EntityUid::from_str(resource)?)); } + let query = builder.context(Context::from_json_str(context, None)?).build(); + // -- begin original -- + // if !context.is_empty() {} + // let principal = EntityUid::from_str(entity)?; + // let action = EntityUid::from_str(action)?; + // let resource = EntityUid::from_str(resource)?; + // let context = Context::from_json_str(context, None)?; + // let query = Request::new(Some(principal), Some(action), Some(resource), context); + // -- end original -- let response = self.authorizer - .is_authorized_partial(&query, &self.policy_set, &self.entity_store); + .is_authorized_partial(&query, &self.policy_set, &self.entity_store.clone().partial()); return match response { PartialResponse::Concrete(concrete) => match serde_json::to_string(&concrete) { Ok(json) => Ok(json), @@ -400,17 +408,32 @@ mod test { entity_store: Entities::empty(), policy_set: PolicySet::new(), }; - let policies = "permit(principal, action, resource); permit(principal, action, resource);"; + let policies = r#"permit(principal == User::"alice", action, resource);"#; engine.set_policies(policies); let entities = "[]"; engine.set_entities(entities); - let result = engine.is_authorized( + let result = engine.is_authorized_partial( "User::\"alice\"", "Action::\"update\"", "Photo::\"VacationPhoto94.jpg\"", "{}", - ); - assert_eq!(result.decision(), Decision::Allow); + ).unwrap(); + + assert_eq!(result, r#"{"decision":"Allow","diagnostics":{"reason":["policy0"],"errors":[]}}"#); + let result = engine.is_authorized_partial( + "User::\"john\"", + "Action::\"update\"", + "Photo::\"VacationPhoto94.jpg\"", + "{}", + ).unwrap(); + assert_eq!(result, r#"{"decision":"Deny","diagnostics":{"reason":[],"errors":[]}}"#); + let result = engine.is_authorized_partial( + "", + "Action::\"update\"", + "Photo::\"VacationPhoto94.jpg\"", + "{}", + ).unwrap(); + assert_eq!(result, "{\"diagnostics\":{\"reason\":[],\"errors\":[]},\"residuals\":\"Templates:\\npermit(\\n principal,\\n action,\\n resource\\n) when {\\n (((unknown(principal) == User::\\\"alice\\\") && true) && true) && true\\n};, Template Linked Policies:\\npermit(\\n principal,\\n action,\\n resource\\n) when {\\n (((unknown(principal) == User::\\\"alice\\\") && true) && true) && true\\n};\"}"); } #[test]