diff --git a/api.go b/api.go index 9dca7d3..3b9bc47 100644 --- a/api.go +++ b/api.go @@ -11,6 +11,7 @@ const ( setPolicies function = "set_policies" isAuthorizedString function = "is_authorized_string" isAuthorizedJSON function = "is_authorized_json" + ffi function = "ffi" ) // exportFuncs returns a map of exported functions from the wasm module. @@ -20,6 +21,7 @@ func exportFuncs(module api.Module) map[string]api.Function { exportedFuncs[string(isAuthorizedJSON)] = module.ExportedFunction(string(isAuthorizedJSON)) exportedFuncs[string(setEntities)] = module.ExportedFunction(string(setEntities)) exportedFuncs[string(setPolicies)] = module.ExportedFunction(string(setPolicies)) + exportedFuncs[string(ffi)] = module.ExportedFunction(string(ffi)) // allocate and deallocate help us manage memory in the wasm module. exportedFuncs[string(allocate)] = module.ExportedFunction(string(allocate)) exportedFuncs[string(deallocate)] = module.ExportedFunction(string(deallocate)) diff --git a/ffi.go b/ffi.go new file mode 100644 index 0000000..8d7291e --- /dev/null +++ b/ffi.go @@ -0,0 +1,59 @@ +package cedar + +import ( + "context" + "encoding/json" + "fmt" +) + +// FFIResponse is the response from the Foreign Function Interface (FFI) of the cedar_policy library. +type FFIResponse struct { + // IsSuccess is true if the request was successful. + // If false, the Errors field will contain the errors that occurred. + IsSuccess bool `json:"success,string,omitempty"` + RawResult string `json:"result,omitempty"` + // Result is the result of the policy evaluation. + Result EvalResponse `json:"-"` + // IsInternal is true if the request failed due to an internal error. + IsInternal bool `json:"isInternal,omitempty"` + // Errors is the list of errors that occurred during evaluation. + Errors []string `json:"errors,omitempty"` +} + +// FFI gives access to the Foreign Function Interface (FFI) of the cedar_policy library. +// See https://docs.rs/cedar-policy/latest/cedar_policy/frontend/is_authorized/fn.json_is_authorized.html for more information. +func (c *CedarEngine) FFI(ctx context.Context, input string) (FFIResponse, error) { + inputSize := uint64(len(input)) + inputPtr, err := c.exportedFuncs[string(allocate)].Call(ctx, inputSize) + if err != nil { + return FFIResponse{}, err + } + defer c.exportedFuncs[string(deallocate)].Call(ctx, inputPtr[0], inputSize) + ok := c.module.Memory().WriteString(uint32(inputPtr[0]), input) + if !ok { + return FFIResponse{}, fmt.Errorf("failed to write input to memory") + } + resPtr, err := c.exportedFuncs[string(ffi)].Call(ctx, inputPtr[0], inputSize) + if err != nil { + return FFIResponse{}, err + } + var res FFIResponse + output, err := c.readDecisionFromMemory(ctx, resPtr[0]) + if err != nil { + return FFIResponse{}, err + } + err = json.Unmarshal(output, &res) + if err != nil { + return FFIResponse{}, err + } + if !res.IsSuccess { + return res, nil + } + var result EvalResponse + err = json.Unmarshal([]byte(res.RawResult), &result) + if err != nil { + return FFIResponse{}, err + } + res.Result = result + return res, nil +} diff --git a/ffi_test.go b/ffi_test.go new file mode 100644 index 0000000..182fc06 --- /dev/null +++ b/ffi_test.go @@ -0,0 +1,85 @@ +package cedar + +import ( + "context" + "testing" +) + +func TestCedarEngine_FFI(t *testing.T) { + ctx := context.Background() + engine, err := NewCedarEngine(ctx) + if err != nil { + t.Fatal(err) + } + defer engine.Close(ctx) + t.Run("ffi must return deny", func(t *testing.T) { + ffiMustReturnDeny(t, engine, ` + { + "principal": "User::\"alice\"", + "action": "Photo::\"view\"", + "resource": "Photo::\"photo\"", + "slice": { + "policies": {}, + "entities": [] + }, + "context": {} + }`) + }) + t.Run("ffi must return error if json is not serializable", func(t *testing.T) { + ffiMustReturnErrorIfJsonIsNotSerializable(t, engine, ` + { + "principal": "User::\"alice\"", + "action": "Photo::\"view\"", + }`) + }) + t.Run("ffi must return allow", func(t *testing.T) { + ffiMustReturnAllow(t, engine, ` + { + "context": {}, + "slice": { + "policies": { + "001": "permit(principal, action, resource);" + }, + "entities": [], + "templates": {}, + "template_instantiations": [] + }, + "principal": "User::\"alice\"", + "action": "Action::\"view\"", + "resource": "Resource::\"thing\"" + }`) + }) +} + +func ffiMustReturnDeny(t *testing.T, engine *CedarEngine, input string) { + ctx := context.Background() + res, err := engine.FFI(ctx, input) + if err != nil { + t.Fatal(err) + } + if res.Result.Decision.IsPermit() { + t.Fatal("expected Deny") + } +} + +func ffiMustReturnErrorIfJsonIsNotSerializable(t *testing.T, engine *CedarEngine, input string) { + ctx := context.Background() + res, err := engine.FFI(ctx, input) + if err != nil { + t.Fatal(err) + } + if len(res.Errors) == 0 { + t.Fatal("expected error") + } +} + +func ffiMustReturnAllow(t *testing.T, engine *CedarEngine, input string) { + ctx := context.Background() + res, err := engine.FFI(ctx, input) + if err != nil { + t.Fatal(err) + } + if !res.Result.Decision.IsPermit() { + t.Fatal("expected Allow") + } +} diff --git a/lib/src/interface.rs b/lib/src/interface.rs index de6843f..7a9cfec 100644 --- a/lib/src/interface.rs +++ b/lib/src/interface.rs @@ -3,7 +3,17 @@ extern crate core; extern crate wee_alloc; extern crate serde_json; -use cedar_policy::{PolicySet, Entities, Authorizer, EntityUid, Context, Request, Decision, Response}; +use cedar_policy::{ + PolicySet, + Entities, + Authorizer, + EntityUid, + Context, + Request, + Decision, + Response, + frontend +}; use std::{slice}; use std::collections::HashMap; @@ -134,6 +144,20 @@ pub unsafe extern "C" fn _is_authorized_json( return ((ptr as u64) << 32) | len as u64; } +#[cfg_attr(all(target_arch = "wasm32"), export_name = "ffi")] +#[no_mangle] +// Provides FFI support +pub unsafe extern "C" fn _ffi( + payload_ptr: u32, + payload_len: u32, +) -> u64 { + let payload = ptr_to_string(payload_ptr, payload_len); + let result = frontend::is_authorized::json_is_authorized(payload.as_str()); + let body = serde_json::to_string(&result).unwrap(); + let (ptr, len) = string_to_ptr(&body); + std::mem::forget(body); + return ((ptr as u64) << 32) | len as u64; +} /// Returns a string from WebAssembly compatible numeric types representing /// its pointer and length. diff --git a/static/cedar.wasm b/static/cedar.wasm index 0bb70c9..02de0fc 100755 Binary files a/static/cedar.wasm and b/static/cedar.wasm differ