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

forbid_read_transfers_over_1000

A forbid rule with a sum over a (value, timepoint) domain and a filtered temporal body. Forbid a Read if the same user’s resolved positive Transfers in the last hour total more than 1000.

The two-binder domain for (a: Long), (t: Timepoint). is what keeps equal amounts made at different timepoints from being deduplicated — a is the summed value and t distinguishes the timepoints, so the sum is per-occurrence.

Because the policy set contains only this forbid rule, no request can ever be allowed — the interesting contrast is the threshold, i.e. whether the forbid fires (an explicit deny, shown as [rules: 0]) or not (a default deny).

The trace shows both sides of the threshold:

  • @0/@1 — alice’s first Transfer resolves to 600 (a history-only event; no matching rule applies, so the decision is a default deny).
  • @10 — alice reads with only 600 transferred in the last hour → default deny (the forbid does not fire; 600 is not over 1000).
  • @20/@21 — a second Transfer for alice resolves to 600 (running total 1200).
  • @30 — alice reads again, now with 600 + 600 = 1200 transferred in the last hour → explicit deny ([rules: 0], the forbid fires because the sum is over 1000).
  • @40 — bob reads with no Transfers of his own → default deny (the per-user pin input.user: context.input.user means alice’s transfers do not count for bob).

Referenced by guide/04-temporal-expressions.md.

Policy

// A FORBID rule with a `sum` over a (value, timepoint) domain and a filtered
// temporal body. The two-binder domain `for (a: Long), (t: Timepoint).` keeps
// equal amounts made at different timepoints from being deduplicated. Forbid a
// Read if the same user's resolved positive Transfers in the last hour total
// more than 1000.
@id("forbid_read_when_transfers_over_1000")
forbid (
    principal,
    action == Drupe::Action::"Read",
    resource
)
when temporal {
    exists (total: Long). (
        (sum a for (a: Long), (t: Timepoint). where (
            formerly within 1h (
                Drupe::Action::"Transfer"::response{ input.user: context.input.user, output.amount: a }
                && a > 0 && tp(t)
            )
        )) == total
        && total > 1000
    )
};

Schema

namespace Drupe {
  type ContentFilterFinding = {
    severityScore: decimal
  };

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

  type LoginOutput = {
    result: Bool
  };

  type PromptAttackFinding = {
    severityScore: decimal
  };

  type ReadInput = {
    document: String,
    user: String
  };

  type ReadOutput = {  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  type TransferInput = {
    user: String
  };

  type TransferOutput = {
    amount: Long
  };

  entity Gateway;

  entity IamEntity = {
    id: String
  };

  entity OAuthUser = {
    id: String
  } tags String;

  entity UnauthenticatedUser;

  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 "Read" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: ReadInput,
      output?: ReadOutput,
      system: SystemContext
    }
  };

  action "Transfer" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: TransferInput,
      output?: TransferOutput,
      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: { user: "alice" }) Drupe::Action::"Transfer"::request(input: { user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@1 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { user: "alice" }, output: { amount: 600 }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "alice" }) Drupe::Action::"Read"::request(input: { document: "doc1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { user: "alice" }) Drupe::Action::"Transfer"::request(input: { user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@21 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { user: "alice" }, output: { amount: 600 }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@30 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "alice" }) Drupe::Action::"Read"::request(input: { document: "doc1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@40 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "bob" }) Drupe::Action::"Read"::request(input: { document: "doc1", user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u5")

Expected Output

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