Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

call_temporal_aggregation_macro_count

Calling a def temporal aggregation macro. count_formerly produces a count, so it is spliced into a comparison and wrapped in an exists binder that introduces the variable it is compared against — never called on its own:

exists (n: Long). (count_formerly(1h, Login) == n && n > 0)

The macro is defined in the attached library macros.dw (supplied via --macros) and desugars to count for ($t: Timepoint). where (formerly within ?w (?s && tp($t))). The policy permits Alert once at least one matching Login is in the last hour.

The trace shows both verdicts:

  • @0 — alice Login on s1 (a history-only event here; no Alert permit applies, so the decision is a deny).
  • @2, @10 — alice Alert on s1, within 1h of her login → allow (count == 1, so n > 0).
  • @20 — bob Login on s1 (history-only again → deny).
  • @22 — alice Alert on s1, still within the window → allow.

Run from this directory:

dogwood validate policy.dw --policy-schema schema.cedarschema --macros macros.dw
dogwood replay   policy.dw --policy-schema schema.cedarschema --macros macros.dw --trace trace.log

Referenced by guide/09-calling-macros.md.

Policy

// Calling a `def temporal` AGGREGATION macro: it produces a count, so
// it is spliced into a comparison inside an `exists` binder (which
// introduces the variable it is compared against) — never called on
// its own. Permits Alert once at least one Login is in the window.
@id("alert_after_a_login")
permit (principal, action == Drupe::Action::"Alert", resource)
when temporal {
    exists (n: Long). (
        (count_formerly(1h, Drupe::Action::"Login"::request{
            input.user: _, input.server: context.input.server
        })) == n
        && n > 0
    )
};

Schema

namespace Drupe {
  type AlertInput = {
    level: Long,
    server: String
  };

  type AlertOutput = {  };

  type ContentFilterFinding = {
    severityScore: decimal
  };

  type LoginInput = {
    server: String,
    user: String
  };

  type LoginOutput = {  };

  type PromptAttackFinding = {
    severityScore: decimal
  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  entity Gateway;

  entity IamEntity = {
    id: String
  };

  entity OAuthUser = {
    id: String
  } tags String;

  entity UnauthenticatedUser;

  action "Alert" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: AlertInput,
      output?: AlertOutput,
      system: SystemContext
    }
  };

  action "CallTool" in [Action::"Mcp"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "Http" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "InvokeAgent" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "InvokeLLM" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "Login" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: LoginInput,
      output?: LoginOutput,
      system: SystemContext
    }
  };

  action "Mcp" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "UnknownTool" in [Action::"CallTool"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };
}

Trace

@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@1 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Login"::response(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@2 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@3 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@11 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@20 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "bob" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@21 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") Drupe::Action::"Login"::response(input: { server: "s1", user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@22 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 3, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 3, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@23 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 3, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u5")

Expected Output

@0 (time point 0): DENY
@2 (time point 1): ALLOW  [rules: 0]
@10 (time point 2): ALLOW  [rules: 0]
@20 (time point 3): DENY
@22 (time point 4): ALLOW  [rules: 0]

Macros

// Macro library supplied via --macros. `count_formerly(?w, ?s)` (?w a bare
// interval, e.g. `1h`) desugars to
//   count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))
// so a call spliced into a comparison inside an `exists` counts how many
// times the predicate ?s held anywhere within the window ?w.
def temporal count_formerly(?w, ?s) {
    count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))
};