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

login_attempt_custom_kind

A custom, author-defined event kind. The per-case event schema (event.dwschema) names attempt as the decision kind and outcome as history (instead of the conventional request/response), and renames the injected principal field to actor (instead of callerPrincipal). The policy permits a Read only if the same actor formerly attempted a Login within the last hour (formerly within 1h, correlating actor: context.principal and input.user: context.input.user).

The event schema is passed with --event-schema event.dwschema; without it the CLI would default to request/response and reject the ::attempt kind.

The trace shows all three cases:

  • @0 — alice attempts a Login (a history-only event here; no Read permit applies to a login, so the decision is a deny).
  • @10 — alice reads, 10s after her login → allow (a matching login by the same actor is within the window).
  • @20 — bob reads with no prior login of his own → deny.

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

Policy

// Event kinds are author-defined. This event schema names `attempt` (the
// decision kind) and `outcome` (history) instead of request/response, and
// renames the injected principal field to `actor`. Permit a Read only if the
// same actor formerly attempted a Login within the last hour.
@id("read_after_login_attempt")
permit (
    principal,
    action == Drupe::Action::"Read",
    resource
)
when temporal {
    formerly within 1h Drupe::Action::"Login"::attempt{
        input.user: context.input.user,
        actor: principal
    }
};

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 = {
    result: Bool
  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  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 "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" }, actor: Drupe::OAuthUser::"alice") Drupe::Action::"Login"::attempt(input: { server: "s1", user: "alice" }, actor: Drupe::OAuthUser::"alice")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "alice" }, actor: Drupe::OAuthUser::"alice") Drupe::Action::"Read"::attempt(input: { document: "doc1", user: "alice" }, actor: Drupe::OAuthUser::"alice")
@20 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "alice" }, actor: Drupe::OAuthUser::"bob") Drupe::Action::"Read"::attempt(input: { document: "doc1", user: "alice" }, actor: Drupe::OAuthUser::"bob")

Expected Output

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

Event Schema

// A custom event schema exercising the per-case override: the event kinds
// are `attempt` (the decision kind) and `outcome` (history), NOT the
// conventional request/response, and the injected principal field is
// named `actor` rather than `callerPrincipal`. This probes that (a) the
// harness honors a per-case event.dwschema, (b) author-defined kinds work
// end to end, and (c) a renamed injected reserved field is usable.
decision event <A>::attempt {
    ...inputs(A),
    actor: principalType(A),
}

event <A>::outcome {
    ...inputs(A),
    ...outputs(A),
    actor: principalType(A),
}