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

alert_same_user_login_and_transfer

A shared exists variable joins two different predicates through a common value: permit an Alert only if the same user both logged in and made a transfer within the last hour. Binding u across both formerlys is the whole point — a broken join that accepted “some login AND some (other-user) transfer” would wrongly permit.

The trace shows both outcomes and the discriminating case:

  • @0 — alice Login (history-only event; no Alert, so deny).
  • @1 — alice Alert with a login but no transfer yet → deny.
  • @2 — alice Transfer (history-only event → deny).
  • @3 — alice Alert after both her login (@0) and transfer (@2) → allow (one user satisfies both sides of the join).
  • @5000 — bob Login; @5001 — carol Transfer (both history-only → deny).
  • @5002 — carol Alert: a login (bob) and a transfer (carol) both exist in the window, but no single user did bothdeny. This is what the shared-u join buys you; a broken join would wrongly allow here.

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

Policy

// A shared `exists` variable JOINS two different predicates: the SAME user
// both logged in and transferred. A broken join that accepted "some login AND
// some (other-user) transfer" would wrongly permit. Permit an Alert if one
// user did both within the last hour.
@id("alert_same_user_login_and_transfer")
permit (
    principal,
    action == Drupe::Action::"Alert",
    resource
)
when temporal {
    exists (u: String). (
        formerly within 1h Drupe::Action::"Login"::request{ input.user: u }
        && formerly within 1h Drupe::Action::"Transfer"::request{ input.user: u }
    )
};

Schema

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

  type AlertOutput = {  };

  type ContentFilterFinding = {
    severityScore: decimal
  };

  type HeartbeatInput = {
    server: String
  };

  type HeartbeatOutput = {  };

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

  type LoginOutput = {  };

  type PromptAttackFinding = {
    severityScore: decimal
  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  type TransferInput = {
    amount: Long,
    user: String
  };

  type TransferOutput = {  };

  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 "Heartbeat" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: HeartbeatInput,
      output?: HeartbeatOutput,
      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 "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: { 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") 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")
@2 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 50, user: "alice" }) Drupe::Action::"Transfer"::request(input: { amount: 50, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@3 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: "u4")
@5000 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: "u5")
@5001 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 50, user: "carol" }) Drupe::Action::"Transfer"::request(input: { amount: 50, user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u6")
@5002 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u7")

Expected Output

@0 (time point 0): DENY
@1 (time point 1): DENY
@2 (time point 2): DENY
@3 (time point 3): ALLOW  [rules: 0]
@5000 (time point 4): DENY
@5001 (time point 5): DENY
@5002 (time point 6): DENY