--- name: Block Public Visibility & Guest Delegation tags: - google-calendar - guard-public-exposure - ingress - soc2 - gdpr-ccpa publishedAt: 2026-07-12 description: | # google-calendar / guard-public-exposure **Direction:** ingress (`tool_pre_invoke`) **Default:** deny on match, allow otherwise **Package:** `google_calendar.ingress.guard_public_exposure` ## What it does Blocks Google Calendar **create** and **update** event calls that would expose the event to the world or hand control of it to guests. A call is denied when any of these appear in its arguments: - `visibility` set to `"public"` — publishes the event body (summary, description, attendees, time) so anyone can read it. - `guestsCanModify` set to `true` — grants every guest, including external ones, edit rights over the event. - `guestsCanInviteOthers` set to `true` — lets guests invite additional people and widen who can see the event. - `anyoneCanAddSelf` set to `true` — lets anyone add themselves as a guest and read the event body. These flags — documented on the `nspady/google-calendar-mcp` `create-event` argument surface — turn a private meeting that carries PHI or deal-sensitive detail into a broadly readable or attacker-editable object. The check runs at ingress, before the call reaches the Calendar MCP server, so the exposed event is never created and never propagated to Google's sharing surfaces. Every other tool call passes through unchanged, and a create/update call with none of these flags set (or all of them at their safe defaults) is allowed. ## Compliance alignment - **SOC 2 CC6.7** — supports the restriction on transmission, movement, and removal of confidential information (PF-05) by stopping the agent from publishing an internal event to a publicly readable scope or delegating its control to outside guests. **P6.1** — supports constraining disclosure of personal information to third parties, since a public or guest-delegable event exposes attendee lists and free-text bodies beyond the org (Partial on the MCP path). - **GDPR Art. 5(1)(f) / Art. 32** — supports security of processing by preventing the agent from exposing attendee personal data (names, email addresses) and event free-text bodies to a publicly readable scope or to externally-delegable guests. **Art. 5(1)(c)** — supports data minimisation by keeping personal data in event bodies from being disclosed beyond its intended internal audience. **CCPA/CPRA §1798.150** — reduces nonredacted-PI exposure by blocking public publication of attendee data. ## Tool name matching The three server families expose the same write operations under different delimiter styles — `create_event` / `update_event` (Google, snake), `create-event` / `update-event` (nspady, kebab), and the consolidated `manage_event` (taylorwilsdon). The policy normalizes `-` to `_` in the tool name and matches the suffixes `create_event`, `update_event`, and `manage_event`. The gateway prepends its own configured server-name prefix (which is not standardized), so matching is by suffix rather than by exact fully-qualified name — a name like `gcal-mcp-create-event` still matches. Both `input.resource.name` (PARC) and `input.payload.name` (legacy tool-hook field) are checked; if **either** names a write-event tool the call is inspected, so a missing or divergent `resource.name` cannot fail the match open. Verify the exact tool name your gateway sends with the dump-input debug technique before relying on this in production. Read tools (`list-events`, `get-event`, …) and destructive tools (`delete-event`, `respond-to-event`) do **not** end in a `create`/`update`/ `manage` `_event` suffix, so they pass through untouched. `manage_event` also spans delete: a delete carries none of the exposure flags, so it is read at its safe defaults and allowed — this policy governs exposure, not deletion (pair it with a destructive-ops guard for that). ## Argument shape Each flag is read with `object.get(args, key, default)` against a **safe** default, so an omitted flag is treated as its non-exposing value and the call is allowed: - `visibility` defaults to `""` (compared case-insensitively to `"public"`, ignoring surrounding whitespace). - `guestsCanModify`, `guestsCanInviteOthers`, `anyoneCanAddSelf` default to `false`. Boolean flags match `true` whether sent as a JSON boolean, as the string `"true"` or `"1"` (any casing, surrounding whitespace ignored — some clients coerce booleans to strings), or as the number `1` (numeric-boolean clients), so a coerced flag cannot slip past the check. If a **write-event** call carries an `args` value that is not a JSON object (a string, array, or number), the exposure checks cannot inspect it, so the call is **denied** (fail closed) with a reason asking for a standard arguments object. A `null` or absent `args` is treated as "no flags set" and allowed. Non-write tools are never affected by this check. ## Examples ### Allowed — no exposure flags ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "google-calendar-mcp-create-event", "type": "tool" }, "payload": { "name": "google-calendar-mcp-create-event", "args": { "summary": "1:1", "start": "…", "end": "…", "visibility": "private" } } } } ``` `allow = true`, no reason. ### Denied — public visibility ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "google-calendar-mcp-create-event", "type": "tool" }, "payload": { "name": "google-calendar-mcp-create-event", "args": { "summary": "Project Atlas M&A sync", "visibility": "public" } } } } ``` `allow = false`, reason explains that public visibility publishes the event and how to fix it. ### Denied — guest control delegated ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "google-calendar-mcp-update-event", "type": "tool" }, "payload": { "name": "google-calendar-mcp-update-event", "args": { "eventId": "abc123", "guestsCanInviteOthers": true } } } } ``` `allow = false`, reason names the `guestsCanInviteOthers` flag. ## Composition Single-purpose. Useful companions from the Calendar candidate set: - An **ingress external-attendee guard** on create/update so invitations to outside domains are controlled alongside public exposure. - An **ingress `sendUpdates` transform** that defaults agent writes to silent so a mistaken create never emails an invitation. - An **egress attendee/PII scrub** on the read tools so previously-created public events are masked when read back. These stay separate policies so each is independently testable and attachable. ## Known limitations - **No break-glass group by default.** Public or self-serve visibility is rarely a legitimate agent action, so no IdP group is exempted. If a tenant needs a break-glass path, add a single `allow if` branch keyed on `input.subject.claims.groups` — e.g. allow when the caller's groups contain a placeholder like `calendar-public-publishers` — using an `object.get(input.subject, "claims", {})` chain so a missing claim fails closed (no group → not exempt → still denied). **Group names are placeholders — replace `calendar-public-publishers` with your IdP's group name at import time.** - **Flag set is fixed.** The policy checks the four documented exposure/ delegation flags. Calendar sharing also has an ACL surface (`acl.insert` with `role: reader` / `scope.type: default`) that the MCP servers in scope do **not** expose as a tool; if a future server surfaces calendar-level ACL writes, extend the matcher and flag list to cover them. - **Argument-key assumptions.** Flag names follow the `nspady` `create-event` surface (Calendar v3 camelCase). A server that renames these (e.g. `guests_can_modify` snake-case) would not be matched — confirm the exact argument keys your server accepts with the dump-input technique and add them to the flag list if they differ. - **Top-level keys only.** Flags are read from the top level of `args`, which is where every in-scope server documents them. A hypothetical server that nests the event body (e.g. `args.event.visibility`) would not be inspected and the call would be allowed — confirm your server's argument shape with the dump-input technique and extend the detections if it nests the body. - **`manage_event` action not inspected.** The consolidated tool is matched by suffix regardless of its action argument; a delete or a read-shaped action simply carries none of the exposure flags and is allowed. This policy does not restrict what `manage_event` does beyond exposure — compose a destructive-ops guard for deletes. > **Compliance note.** This policy supports alignment with the cited framework controls **on the MCP path only**. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it. direction: ingress apps: - google-calendar industries: [] bundles: - soc2 - gdpr-ccpa experimental: true schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package google_calendar.ingress.guard_public_exposure # Deny-by-default: only the explicit allow rules below permit the request. default allow := false # --- Tool matching ------------------------------------------------------------ # The write operations appear under three delimiter styles across servers: # create_event / update_event (Google, snake_case) # create-event / update-event (nspady, kebab-case) # manage_event (taylorwilsdon, consolidated create/update/delete) # Normalize `-` to `_` and match by suffix so the gateway's configured # server-name prefix (e.g. `google-calendar-mcp-`) does not defeat the match. # Both the PARC field (resource.name) and the legacy tool-hook field # (payload.name) are checked: if either names a write-event tool the call is # inspected, so a missing or divergent resource.name cannot fail the match open. normalized_name(raw) := replace(lower(raw), "-", "_") tool_names contains normalized_name(object.get(object.get(input, "resource", {}), "name", "")) tool_names contains normalized_name(object.get(object.get(input, "payload", {}), "name", "")) write_suffixes := {"create_event", "update_event", "manage_event"} is_write_event_tool if { some name in tool_names some suffix in write_suffixes endswith(name, suffix) } # --- Arguments ---------------------------------------------------------------- # Read the args bag defensively; a missing (or null) payload/args yields {} so # every flag below resolves to its safe default. A present-but-non-object args # value (string, array, number) is flagged as malformed instead — see # malformed_args below — because none of the detections could inspect it. raw_args := object.get(object.get(input, "payload", {}), "args", {}) tool_args := raw_args if is_object(raw_args) tool_args := {} if not is_object(raw_args) # Fail closed when a write-event call carries args the detections cannot read. # (null is treated like missing args — safe defaults — not as malformed.) malformed_args if { not is_object(raw_args) raw_args != null } # A flag counts as "on" when it is boolean true, the string "true" or "1" # (compared case-insensitively, ignoring surrounding whitespace — some clients # coerce booleans to strings on the wire), or the number 1 (numeric-boolean # clients). flag_true(key) if { object.get(tool_args, key, false) == true } flag_true(key) if { v := object.get(tool_args, key, false) is_string(v) truthy_strings[lower(trim_space(v))] } truthy_strings := {"true", "1"} flag_true(key) if { object.get(tool_args, key, false) == 1 } # --- Exposure / delegation detections ----------------------------------------- public_visibility if { v := object.get(tool_args, "visibility", "") is_string(v) lower(trim_space(v)) == "public" } guests_can_modify if { flag_true("guestsCanModify") } guests_can_invite_others if { flag_true("guestsCanInviteOthers") } anyone_can_add_self if { flag_true("anyoneCanAddSelf") } exposes_event if { public_visibility } exposes_event if { guests_can_modify } exposes_event if { guests_can_invite_others } exposes_event if { anyone_can_add_self } # --- Allow rules -------------------------------------------------------------- # Pass through anything that is not a create/update event write. allow if { not is_write_event_tool } # Allow create/update writes that carry no exposure or delegation flag and # whose arguments were actually inspectable. allow if { is_write_event_tool not exposes_event not malformed_args } # --- Deny reasons ------------------------------------------------------------- reasons contains "This event sets visibility to \"public\", which publishes its summary, description, attendees, and time to anyone. Set visibility to \"private\" or \"default\" before creating or updating the event. Contact your security team if a public event is genuinely required." if { is_write_event_tool public_visibility } reasons contains "This event sets guestsCanModify to true, granting every guest — including any external attendees — edit rights over the event. Remove guestsCanModify (or set it to false) before creating or updating the event. Contact your security team if delegated edit access is genuinely required." if { is_write_event_tool guests_can_modify } reasons contains "This event sets guestsCanInviteOthers to true, letting guests invite others and widen who can see the event. Remove guestsCanInviteOthers (or set it to false) before creating or updating the event. Contact your security team if this is genuinely required." if { is_write_event_tool guests_can_invite_others } reasons contains "This event sets anyoneCanAddSelf to true, letting anyone add themselves as a guest and read the event body. Remove anyoneCanAddSelf (or set it to false) before creating or updating the event. Contact your security team if this is genuinely required." if { is_write_event_tool anyone_can_add_self } reasons contains "This event write's arguments are not a JSON object, so the exposure checks cannot inspect them. Resend the call with a standard arguments object. Contact your security team if this keeps happening." if { is_write_event_tool malformed_args } reason := joined if { count(reasons) > 0 reason_list := sort([r | some r in reasons]) joined := concat("; ", reason_list) } ```