# Connector Permission Bundles

This document is guidance for the five connector permission keys and for how an
administrator grants connector access today. The two named compositions below
(`connector.observer` / `connector.operator`) are **illustrative future L1/L2
PermissionSet templates**, not a canonical artifact this repo ships. Only the
five keys are owned by platform code; bundle names and key membership are
application-shaped policy that will live in L1/L2 PermissionSet metadata, never
in this document or in tests.

## The five connector permission keys

The connector substrate gates its privileged surfaces with five platform
permission keys. They are catalog rows in
`backend/internal/bootstrap/standard_permissions.json` (category `Integration`,
`defaultGrantedTo: ["SystemAdmin"]`) and typed constants in
`shared/pkg/constants/permissions.go`:

| Key | Gates |
|---|---|
| `ManageNamedCredentials` | NamedCredential definition/value rotate; WebhookSecret rotate (shared bundle) |
| `ViewConnectorHealth` | `GET /api/connectors/{namespace}/health` |
| `ViewConnectorEvents` | Read the connector dead-letter queue (DLQ) |
| `ReplayConnectorEvents` | Replay a DLQ event (distinct from read, so an observer can audit without acting) |
| `ViewConnectorDiagnostics` | Per-record connector sync-status diagnostics read (`GET /admin/connector/sync-status`) — destination lineage + last-attempt status. |

These keys are the single owner of connector authz names. New connector
surfaces reference them by constant, never by magic string.

## Two example compositions (not shipped as metadata)

These two connector-neutral compositions are the intended shape of the future
PermissionSet templates, documented here for design intent only. The repo does
not ship them as PermissionSet rows today, and their canonical owner — once
shipped — is L1/L2 PermissionSet metadata, not this document. They are
connector-neutral: no `hubspot.*` or vendor-specific bundle.

| Composition | Keys | Intended audience |
|---|---|---|
| `connector.observer` | `ViewConnectorEvents`, `ViewConnectorHealth` | On-call / rotation members who watch connector status and audit the DLQ but do not act. Read-only. |
| `connector.operator` | `ManageNamedCredentials`, `ViewConnectorEvents`, `ReplayConnectorEvents`, `ViewConnectorHealth` | Integration operators who can rotate secrets and replay DLQ events, in addition to everything an observer can do. |

The only part exercised by code today is the read-vs-replay split on the DLQ
service. Behavior tests in
`backend/internal/features/connector/events/service_test.go`
(`TestConnectorObserverBundle_*`, `TestConnectorOperatorBundle_*`) confirm that
an actor whose key set lacks `ReplayConnectorEvents` cannot replay a DLQ event,
while an actor that holds it clears the replay gate. Those tests exercise only
the `ViewConnectorEvents` / `ReplayConnectorEvents` gates owned by this service;
they do not assert the `ViewConnectorHealth` or `ManageNamedCredentials`
membership of the example compositions, which is gated in other packages.

## Assignment story

Connector access is granted through the platform's existing PermissionSet
resolution — there is no connector-specific authz path. Effective system
permissions for a user resolve from two implemented sources
(`GetEffectiveSystemPermissions`,
`backend/internal/infrastructure/persistence/perm_system.go`):

1. the user's profile-owned PermissionSet, and
2. any PermissionSet (or PermissionSetGroup) directly assigned to that user.

Assignment is **user-level**: `PermissionSetAssignment` and
`PermissionSetGroupAssignment` are keyed by `AssigneeId = userID`, not by profile
or role. So an administrator grants connector access by one of the implemented
paths:

- add the connector keys to a profile-owned PermissionSet — every user on that
  profile then inherits them; or
- author a PermissionSet that bundles the connector keys and assign it to the
  specific users who need it.

The second path is **L2 admin/tenant configuration**, authored at runtime
against a live org. It is deliberately outside clean-L0 verification:
`checkPermissionSetInventory` (`backend/internal/controlplane/verify_l0.go`)
flags any non-profile-owned PermissionSet in a freshly bootstrapped L0, so such
bundles are never part of the greenfield public-core baseline — they live only
in a configured org, never in clean L0.

## Webhook ingress is exempt

Inbound webhook verification is **not** gated by any of these five keys. HMAC
signature verification *is* the authorization for inbound webhooks: the request
is trusted because it carries a valid signature for a known connector namespace,
not because a caller holds a platform permission. The inbound dispatcher must
not call `RequirePermission` for `ViewConnectorEvents` / `ReplayConnectorEvents`
/ `ViewConnectorHealth` / `ManageNamedCredentials`. When the dispatcher lands,
its own tests own this guarantee.

## Status and the path to assignable templates

What ships today: the five keys exist as enforceable platform permissions and
the DLQ service references them by constant. The observer/operator compositions
above are documented design intent, not a shipped artifact.

What is deferred: shipping `connector.observer` / `connector.operator` as
**assignable PermissionSet metadata**. A pre-built standalone (non profile-owned)
PermissionSet is rejected by clean-L0 verification (`checkPermissionSetInventory`),
because the platform binary is intentionally blind to application-shaped policy —
"which keys compose which bundle" is an L1 distribution default or an L2 admin
decision, not L0 substrate. The canonical bundle definitions therefore belong in
a connector-neutral L1 pack (or L2 admin config), authored once that pack layer
exists. They must not be forced into L0 platform code — nor pinned as a parallel
source of truth in docs or tests — by relaxing the clean-L0 ownership invariant.
