Skip to content

EPIC Q — Scoped Provider Connection Bindings #99

Description

@haydercyber

Summary

Adds project-scoped self-service binding of provider connections, so section heads can bind/unbind platform-approved provider connections on their own projects without holding the global integration.edit permission.

Builds on EPIC P (#92) which shipped the provider connection admin surface + the project_provider_connections join table.

Why

Today integration.edit is a single binary global permission. Every holder sees every provider connection across every project, edits any, deletes any. With the polyrepo + tenancy model EPIC L + N built (section heads owning project subtrees), this is too blunt. EPIC Q ships the minimum useful split: connections stay platform-managed, bindings become project-scoped.

Locked design decisions (§1–§6)

Q1  — connections global, bindings scoped (Option C)
Q2  — new integration.bind permission, integration.edit unchanged
Q3  — bind-only for scoped admins, no create/update/delete
Q4  — hard block prod for scoped binders (Option A); per-connection prod
      opt-in parked as Q-follow-up #1
Q5  — 3 stable codes + environment_id_required for the binder branch
Q6  — new provider_connection_binder seed role; not added to developer
Q7  — unbind skips self_service_bindable but enforces scope + non-prod
Q8  — extend shared GET with ?for_binding=true (4-way branching)
Q9  — per-project/per-env Provider Connections card on ProjectEnv only
Q10 — project-anchored URLs alongside admin URLs
Q11 — new GET /projects/:id/provider-connection-bindings with server join
Q12 — card placement: ProjectEnv only (no /projects/:id overview page)
Q13 — show project-wide bindings with badge + disabled Unbind
Q14 — empty-state copy split: pure-bind users see no link; admins see
      "Manage at /admin/provider-connections"
Q15 — capability helper (NOT generic hasPermission override);
      explicit endpoint selection by permission
Q16 — 3 new prometheus counters (created/deleted/denied)
Q17 — 5-slice PR breakdown

Load-bearing gate-order correction (§3): project/env coverage check runs BEFORE connection load to prevent enumeration leak.

Scoped DELETE: binding.project_id == URL projectID validated; mismatch returns binding_not_found (not out_of_scope_binding, which would leak existence).

Picker excludes connections that are project-wide-bound to the project (transparency about inherited bindings).

Default deny: self_service_bindable BOOLEAN NOT NULL DEFAULT false.

Schema (migration 0031)

ALTER TABLE provider_connections
    ADD COLUMN self_service_bindable BOOLEAN NOT NULL DEFAULT false;

No new tables. No FK changes. Existing connections stay invisible to scoped binders until platform explicitly opts each in.

Sub-issues

  • Q1 — schema + storage + service + permission catalog
  • Q2 — handlers + envelope + observability
  • Q3 (ui) — per-project Provider Connections card + binder picker
  • Q4 (docs) — Scoped bindings operator guide additions
  • Q5 — EPIC Q skills round-trip

Q-follow-ups (NOT v1 blockers)

Stable error codes added

connection_not_self_service_bindable      403
prod_binding_not_allowed_for_scope        403
out_of_scope_binding                      403
environment_id_required                   400

Existing codes reused: connection_disabled (409), connection_not_found (404), binding_exists (409), binding_not_found (404), environment_not_in_project (400).

Audit events

  • binding.create — successful bind via either path; metadata carries actor_permission_used so platform can grep scoped vs admin
  • binding.delete — successful unbind; includes self_service_bindable when connection row still loadable, omits when not
  • binding.denied_out_of_scope — step-2 failure on scoped path; does NOT include provider_connection_id (gate-order protection)

Observability

Three prometheus counters (low-cardinality labels only — never actor_id/project_id/connection_id/environment_id):

provider_connection_bindings_created_total{permission_used, env_kind}
provider_connection_bindings_deleted_total{permission_used, env_kind}
provider_connection_bindings_denied_total{reason}

reason fixed set: out_of_scope, prod_blocked, not_self_service_bindable, connection_disabled, connection_not_found, env_not_in_project, binding_exists, binding_not_found.

Test plan (cross-slice)

Beyond the per-PR test scope:

  • Scoped user cannot bind prod
  • Scoped user cannot unbind prod
  • Scoped user cannot create project-wide binding
  • Scoped user cannot delete project-wide binding
  • Project-wide inherited binding appears read-only on ProjectEnv
  • Binder picker excludes project-wide already-effective connections

Dependencies

Definition of done

  • Q1–Q5 merged
  • All 4 follow-ups filed with intentional priority labels
  • EPIC Q skills round-trip merged
  • Operator guide live on docs site

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicTracking issue spanning multiple PRspriority/p1Should-have; post-MVP soon

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions