Skip to content

Privatelinks GraphQL#2944

Draft
jshearer wants to merge 6 commits into
masterfrom
jshearer/private_links_gql
Draft

Privatelinks GraphQL#2944
jshearer wants to merge 6 commits into
masterfrom
jshearer/private_links_gql

Conversation

@jshearer
Copy link
Copy Markdown
Contributor

WIP

jshearer added 6 commits May 13, 2026 11:13
…te/` and `ops/tasks/private/` prefixes

The generic invite-link APIs (`createInviteLink`, `deleteInviteLink`, `inviteLinks`) should not let users manage or discover delegation under the platform-owned `ops/dp/private/<tenant>/` and `ops/tasks/private/<tenant>/` prefixes. Those grants are derived from the customer catalog prefix at data-plane provisioning time, so direct user-driven invite links there would bypass that ownership relationship.

* createInviteLink and deleteInviteLink reject those prefixes before authorization, so even an explicit admin grant on an internal prefix cannot be used to mint or revoke a delegation link there.
* inviteLinks filters those prefixes out of the listing query as defense-in-depth against directly-inserted rows or unexpected admin grants.
* `ensure_private_data_plane_grants()` is unchanged; its sub-prefix `read` grants are a separate RLS workaround.
…oller

The GraphQL API needs typed visibility into the same `PrivateLink` shape the data-plane controller (DPC) reads from the `private_links json[]` column. Define the types in `models` so both crates share a single source of truth, and re-export from `data-plane-controller::shared::stack` so existing DPC callers keep compiling unchanged.

* Serde shape is preserved byte-for-byte: untagged enum, declaration order AWS/Azure/GCP, Azure `dns_name`/`resource_type` stay as `String` with `skip_serializing_if = "String::is_empty"`, GCP `all_ports` default/skip behavior unchanged.
* Output-side async-graphql derives are added under the existing `async-graphql` feature: `SimpleObject` on each provider struct with explicit GraphQL names, and `Union` on `PrivateLink`. The mutation input type is intentionally left to the GraphQL layer (Phase 4) so the union member names and the one-of field names can be set independently.
…isioning-endpoint fields

The `dataPlanes` query now exposes the configured private link config typed against the shared `models::PrivateLink` union, plus the three provisioning-result endpoint columns (`awsLinkEndpoints`, `azureLinkEndpoints`, `gcpPscEndpoints`) as opaque JSON arrays. Raw `private_links` rows are fetched once per page and parsed lazily so a malformed historical row produces a descriptive field error naming the data plane and link index, rather than breaking the whole query selection.

* fetch_data_plane_details extended to SELECT the four new columns; the three endpoint columns are nullable `json[]` and map NULL to an empty Vec.
* private_links.sql fixture adds a private data plane with one entry of each variant plus an AWS provisioning result, and the `aliceCo/` → `ops/dp/private/aliceCo/` role grant so authorization picks the row up.
… mutation

Adds a single mutation that replaces the entire `private_links` column for a private data plane. The provided list is the new full state; the data-plane controller converges to it on its next poll.

Authorization is the interim shape that mirrors the existing data-plane deployment model: `read` on the private data-plane name is sufficient. This will be replaced with `manage_dataplane` once the orthogonal capability model lands.

* `OneofObject` + `InputObject` derives added to `models::PrivateLink` and its provider structs, so the mutation accepts `Vec<models::PrivateLink>` directly with no input-side type duplication or conversion shim. Output and input GraphQL types are generated from the same Rust source.
* Name parsing rejects public data planes and anything that doesn't strictly match `ops/dp/private/<tenant>/<provider>-<region>-c<n>`, before authorization or any DB write.
* Per-provider validation: AWS `service_name` matches `com.amazonaws.vpce.<region>.vpce-svc-*` with matching region; GCP `service_attachment` matches `projects/*/regions/*/serviceAttachments/*` with matching region; Azure requires `service_name` and `location`; required arrays must be non-empty.
* Returns the post-write `DataPlane` (re-fetched via the same path as the `dataPlanes` query) so callers see the bumped `updated_at` and canonical parsed `privateLinks`.
These fields are semantically optional in the est-dry-dock Pulumi contract. Previously both `models::PrivateLink` and the generated GraphQL schema represented them as required `String` with the empty string as the "absent" sentinel, producing a `String!` GraphQL contract that required clients to know about the empty-string convention.

Change the model to `Option<String>` with a serde adapter that maps incoming missing fields and incoming empty strings to `None`, and serializes both `None` and `Some("")` to a missing field. The wire format is byte-for-byte unchanged: historical rows with either representation round-trip identically. The Pulumi consumer (est-dry-dock) already receives "field absent" via `skip_serializing_if`; its Pydantic model defaults the field back to `""` independently and checks via `!= ""` / truthy, so no est-dry-dock change is required.

The GraphQL surface now exposes `dnsName: String` and `resourceType: String` (nullable) on both `AzurePrivateLink` and `AzurePrivateLinkInput`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant