Skip to content

TPE: unable to serialize a residual with an error() node to JSON #2202

@evanmoses-okta

Description

@evanmoses-okta

Before opening, please confirm:

Describe the bug

Using Cedar 4.9.1, When you attempt to serialize a residual that contains an error() node to JSON, you get an error "Failed to serialize residual to JSON: error is not a valid function".

The program below makes an is_authorized_partial that results in a residual with an error node because the resource in question has no assignedTo property. It produces the following output :

Decision: None
Residual policy (cedar text): permit(
  principal,
  action,
  resource
) when {
  (((context.commonConditionals) has mfa) && false) && (error())
};
ERROR: Failed to serialize residual to JSON: `error` is not a valid function
  Policy ID: policy0
  Cedar text: permit(
  principal,
  action,
  resource
) when {
  (((context.commonConditionals) has mfa) && false) && (error())
};

We should still be able to serialize that residual to JSON and evaluate it in the calling program, since at the end of the day the condition resolves to false and there isn't actually an error.

Discussed on the Cedar slack, thread here: https://cedar-policy.slack.com/archives/C070BAB44FP/p1772647162731879

Reproduction steps

use cedar_policy::{
    Entities, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, PolicySet, Schema,
    Validator,
};

const SCHEMA_CEDAR: &str = r#"
entity User = {
    name: String,
};

entity Account = {
    name: String,
    assignedTo?: User,
};

type MFAConditional = {
    met: Bool,
};

type CommonConditionals = {
    mfa?: MFAConditional,
};

action RevealCredentials appliesTo {
    principal: [User],
    resource: [Account],
    context: {
        commonConditionals: CommonConditionals,
    },
};
"#;

// A policy with a "has" guarding access to assignedTo
const POLICY: &str = r#"permit (
    principal is User,
    action == Action::"RevealCredentials",
    resource is Account
)
when { context.commonConditionals has mfa && resource has assignedTo && resource.assignedTo == principal };"#;

// Note that there's no `assignedTo` on the account's attrs
const ENTITIES_JSON: &str = r#"[
  {
    "uid": {
      "type": "User",
      "id": "user1"
    },
    "parents": [],
    "attrs": {
      "name": "testuser"
    },
    "tags": {}
  },
  {
    "uid": {
      "type": "Account",
      "id": "account1"
    },
    "parents": [],
    "attrs": {
      "name": "sharedaccount1"
    },
    "tags": {}
  }
]"#;

fn main() {
    // Parse schema from Cedar human-readable format
    let (schema, _warnings) =
        Schema::from_cedarschema_str(SCHEMA_CEDAR).expect("Failed to parse schema");

    // Parse and validate policy
    let policy_set: PolicySet = POLICY.parse().expect("Failed to parse policy");
    let validator = Validator::new(schema.clone());
    let validation_result =
        validator.validate(&policy_set, cedar_policy::ValidationMode::default());
    assert!(
        validation_result.validation_passed(),
        "Policy validation failed: {:?}",
        validation_result.validation_errors().collect::<Vec<_>>()
    );

    // Parse entities
    let entities =
        Entities::from_json_str(ENTITIES_JSON, Some(&schema)).expect("Failed to parse entities");
    let partial_entities = PartialEntities::from_concrete(entities, &schema)
        .expect("Failed to create partial entities");

    // Build partial request with no context (context is unknown)
    let principal = PartialEntityUid::from_concrete(
        r#"User::"user1""#.parse::<EntityUid>().expect("Failed to parse principal"),
    );
    let action: EntityUid = r#"Action::"RevealCredentials""#
        .parse()
        .expect("Failed to parse action");
    let resource = PartialEntityUid::from_concrete(
        r#"Account::"account1""#.parse::<EntityUid>().expect("Failed to parse resource"),
    );

    let request = PartialRequest::new(principal, action, resource, None, &schema)
        .expect("Failed to create partial request");

    // Run TPE
    let response = policy_set
        .tpe(&request, &partial_entities, &schema)
        .expect("TPE evaluation failed");

    println!("Decision: {:?}", response.decision());

    // Attempt to serialize residual policies to JSON — this is where the error occurs
    for policy in response.nontrivial_residual_policies() {
        println!("Residual policy (cedar text): {}", policy);
        match policy.to_json() {
            Ok(json) => println!("Residual policy (json): {}", json),
            Err(e) => {
                eprintln!("ERROR: Failed to serialize residual to JSON: {}", e);
                eprintln!("  Policy ID: {}", policy.id());
                eprintln!("  Cedar text: {}", policy);
                panic!("to_json() failed on residual policy — this is the bug");
            }
        }
    }

    println!("All residual policies serialized successfully (no bug).");
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working. This is as high priority issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions