Skip to content

Add cloud accounts (AWS / GCP / Azure connectors)#1138

Open
aureliensibiril wants to merge 35 commits into
mainfrom
aureliensibiril/cloud_connectors
Open

Add cloud accounts (AWS / GCP / Azure connectors)#1138
aureliensibiril wants to merge 35 commits into
mainfrom
aureliensibiril/cloud_connectors

Conversation

@aureliensibiril
Copy link
Copy Markdown
Collaborator

@aureliensibiril aureliensibiril commented May 1, 2026

Summary

Ships a versatile, provider-extensible cloud-account connector for AWS, GCP, and Azure that feeds the existing access-review pipeline today and lays the groundwork for broader CSPM-style security assessment tomorrow.

A CloudAccount is a tenant-scoped, polymorphic record holding (provider, credential_kind, scope, encrypted_credentials, status). The new pkg/cloudaccount package provides a Provider abstraction layered over the AWS SDK v2, the Google Cloud Go SDK, and the Azure SDK for Go. The access-review driver dispatch grows three new branches (cloud_aws, cloud_gcp, cloud_azure) that consume a Provider and produce the same []AccountRecord shape every other source produces today. A new settings tab in apps/console next to SCIM hosts a per-cloud install wizard.

Highlights

  • AWS install: customer clicks "Connect AWS", enters a label, the UI builds a CloudFormation Quick-Create URL pointing at a content-addressed S3 template (object key embeds the SHA-256 — the URL itself is the integrity pin) with a per-connection ExternalId parameter; backend probes via sts:AssumeRole + sts:GetCallerIdentity + iam:ListUsers.
  • GCP install: scope-dependent role grants — project scope grants roles/iam.securityReviewer; org scope additionally grants roles/cloudasset.viewer to unlock the richer transitive enumeration; gcloud setup script generated server-side.
  • Azure install: single-tenant App Registration with a structured walkthrough including the mandatory "Grant admin consent" step for Directory.Read.All.
  • Status machine with three-strikes + 1h time gate: PENDING_VERIFICATIONVERIFIEDERROREDDISCONNECTED (only after 3 consecutive failures spanning ≥1h).
  • Periodic probe worker built on kit/worker mirroring the canonical evidence_description_worker shape, with FOR UPDATE SKIP LOCKED claims, deterministic state-transition logic, and webhook emission only on ERRORED → VERIFIED recovery.
  • Secret credentials never touch GraphQL or React state: GCP service-account JSON keys and Azure client secrets travel through a dedicated POST /api/console/v1/cloud-accounts/credentials/upload multipart endpoint, which is excluded from the access-log middleware so the body never lands in any log file. Frontend uses useRef (not useState) for the secret-paste field, with autocomplete="off", spellcheck="false", data-1p-ignore, and -webkit-text-security: disc masking.
  • Three-interface sync: each operation appears in GraphQL, MCP (7 tools), and CLI (prb cloud-account list / get / create / verify / rotate / delete / install-assets).
  • Field-level RBAC: lastProbeError and scope.identifier are restricted to OWNER/ADMIN via forceResolver field resolvers — auditors can see which clouds are connected (evidence for attestations) but never the credential-adjacent surfaces.
  • Request-scoped authz cache on the composite (subject, organization, action) key: a 25-row list with three permission flags drops from 75 pg.WithTx authorize calls to 3.

Architecture invariants

  • pkg/cloudaccount does NOT import the data-layer entity (grep "coredata.CloudAccount{" pkg/cloudaccount/ is empty); it operates on a thin CloudAccountRecord value type.
  • pkg/accessreview engine internals stay SDK-free (no AWS / GCP / Azure SDK imports, no direct cloud-account coredata reads); driver construction is injected via a CloudAccountDriverFactory closure built in pkg/probo.
  • Access-source FK to cloud_accounts is ON DELETE RESTRICT; the service maps the PG 23503 violation to coredata.ErrResourceInUse so the resolver returns Conflict, not Internal.
  • The check constraint on access_sources (exactly one of {connector_id, cloud_account_id, csv_data}) lands as NOT VALID + a separate VALIDATE CONSTRAINT so the migration acquires only ShareUpdateExclusiveLock instead of locking out reads.

Test plan

Backend (174 test functions across 24 files):

  • pkg/cloudaccount/... — credentials envelope round-trip, AWS/GCP/Azure provider seams, install asset shape (admin-consent step required), MapSDKError per-SDK fingerprinting, registry polymorphic dispatch, install_aws / install_gcp / install_azure shape, permissions matrix
  • pkg/coredata/cloud_account_test.go — entity Insert/Update/Delete, encrypted-credentials cipher round-trip, Scoper rejection cross-tenant, paginated filter, LoadNextStaleForUpdateSkipLocked concurrent claim race
  • pkg/probo/cloud_account_service_test.go — Validate purity (no DB I/O), per-(provider, kind) marshaling discriminator, Create / Verify / RotateCredentials / Delete flows, FK-violation mapping, external_id reuse defeat
  • pkg/probo/cloud_account_worker_test.go — state machine transitions (success/failure/recovery/disconnect), Claim returns worker.ErrNoTask on no rows, concurrent-claim race
  • pkg/accessreview/drivers/cloud_{aws,gcp,azure}_test.go — IAM users + MFA, GCP scope-dependent fetch path (project: cloudresourcemanager only; org: + cloudasset), Azure role assignments + Graph batch lookup
  • pkg/server/api/console/v1/authzcache_test.go — composite-key cache, mutation bypass, per-request isolation, 100-goroutine concurrent reads
  • pkg/server/api/console/v1/cloud_account_credential_upload_test.go — first-attach vs rotation distinction, body cap, JSON envelope shape
  • pkg/server/api/mcp/v1/cloud_account_test.go — per-tool action mapping + RBAC matrix
  • pkg/cmd/cloud-account/.../*_test.go — flag parsing, output formats, GraphQL mutation construction for all 7 subcommands
  • e2e/console/cloud_account_test.go — 12 subtests including the explicit 35-row RBAC matrix, tenant isolation, external_id reuse defeat, redaction tests, access-review integration, FK-blocked delete

Frontend (28 vitest cases):

  • packages/helpers/src/cloudAccounts.test.ts — provider / status / scope-kind helper exhaustiveness
  • packages/helpers/src/cloudAccountWizard.test.ts — three reducer step machines (AWS / GCP / Azure)

Notes for reviewers

  • The 19 commits are organized topologically (foundation → consumers) so each represents a coherent unit. The MCP commit bundles the access-source cloud_account_id field forwarding because mcpgen regenerates schema.resolvers.go atomically.
  • Pre-existing branch issue unrelated to this PR: packages/emails/dist/ is empty and blocks pkg/probo runtime tests until make @probo/emails is run.
  • The plan and full implementation history is captured in contrib/claude/cloudaccount.md.

Summary by cubic

Adds multi‑cloud cloud‑account connectors for AWS, GCP, and Azure with secure credential upload, periodic health probes, lifecycle webhooks, and full UI/CLI/MCP support. Introduces org‑scoped RBAC actions and access‑review drivers; makes AWS install‑assets opt‑in.

  • Bug Fixes

    • GCP: keep bare IAM members (e.g. allUsers) in access‑review output; drop conflicting option.WithHTTPClient; single‑quote vars in the install script.
    • Azure: make role‑assignment aggregation order‑independent by promoting better email/name when available.
    • Console: reload Cloud Accounts when the organization changes.
    • Access sources: enforce target mutual exclusivity on update by clearing other fields; allow target‑less rows; add cross‑org ownership check; return non‑revealing errors on mismatches.
    • Coredata: validate cloud‑account enum boundaries to reject unsupported values.
  • Migration

    • Run DB migrations for cloud_accounts and link access_sources.cloud_account_id with the “at most one target” CHECK across {connector_id, cloud_account_id, csv_data}.
    • Configure probod cloud‑account settings (pkg/probodconfig.CloudAccountConfig) and start the probe worker. AWS install‑assets now requires CLOUD_ACCOUNT_AWS_* values (template URL, SHA‑256, assumer ARNs); without them, AWS assets return UNAVAILABLE by design.
    • Rebuild and deploy Console/Server to expose the new Cloud Accounts settings page, install wizards, APIs, and CLI (prb cloud-account …).

Written for commit 612e56e. Summary will update on new commits.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

eslint (apps/console)

🚫 [eslint (apps/console)] reported by reviewdog 🐶
: must be placed on a new line @stylistic/jsx-one-expression-per-line


🚫 [eslint (apps/console)] reported by reviewdog 🐶
{" "} must be placed on a new line @stylistic/jsx-one-expression-per-line


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected newline before '}' @stylistic/jsx-curly-newline


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected newline before '}' @stylistic/jsx-curly-newline


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens

const cloudAccounts = data.cloudAccounts.edges.map((edge) => edge.node);


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens

(account) => account.canRotate || account.canVerify || account.canDelete,


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'??' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Expected newline between test and consequent of ternary expression @stylistic/multiline-ternary


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Expected newline between consequent and alternate of ternary expression @stylistic/multiline-ternary


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Async arrow function 'handleSubmit' has no 'await' expression @typescript-eslint/require-await


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
'=' should be placed at the beginning of the line @stylistic/operator-linebreak


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces @stylistic/arrow-parens


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected newline before '}' @stylistic/jsx-curly-newline


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected newline before '}' @stylistic/jsx-curly-newline


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Unexpected newline before '}' @stylistic/jsx-curly-newline


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Expected newline between test and consequent of ternary expression @stylistic/multiline-ternary


🚫 [eslint (apps/console)] reported by reviewdog 🐶
Expected newline between consequent and alternate of ternary expression @stylistic/multiline-ternary


🚫 [eslint (apps/console)] reported by reviewdog 🐶
{__("Roles granted")} must be placed on a new line @stylistic/jsx-one-expression-per-line


🚫 [eslint (apps/console)] reported by reviewdog 🐶
: must be placed on a new line @stylistic/jsx-one-expression-per-line


🚫 [eslint (apps/console)] reported by reviewdog 🐶
{" "} must be placed on a new line @stylistic/jsx-one-expression-per-line

@aureliensibiril
Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 1, 2026

@cubic-dev-ai review this PR

@aureliensibiril I have started the AI code review. It will take a few minutes to complete.

@aureliensibiril aureliensibiril marked this pull request as ready for review May 1, 2026 20:52
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 119 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritises the most important files to review.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found across 119 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritises the most important files to review.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pkg/coredata/cloud_account_credential_kind.go">

<violation number="1" location="pkg/coredata/cloud_account_credential_kind.go:76">
P2: `Value()` should validate enum values before returning them; currently it will persist arbitrary strings for this credential kind.</violation>
</file>

<file name="pkg/accessreview/drivers/cloud_gcp.go">

<violation number="1" location="pkg/accessreview/drivers/cloud_gcp.go:155">
P1: Valid GCP IAM members without a colon (for example `allUsers`) are dropped, causing public bindings to be omitted from account review results.</violation>
</file>

<file name="apps/console/src/pages/organizations/settings/cloud-accounts/CloudAccountsSettingsPageLoader.tsx">

<violation number="1" location="apps/console/src/pages/organizations/settings/cloud-accounts/CloudAccountsSettingsPageLoader.tsx:34">
P1: Reload logic is tied to `queryRef` instead of `organizationId`, so switching organizations can keep stale query data. Scope the effect to `organizationId` (and `loadQuery`) and always load for the current org.</violation>
</file>

<file name="pkg/accessreview/drivers/cloud_azure.go">

<violation number="1" location="pkg/accessreview/drivers/cloud_azure.go:91">
P2: Aggregating by principal currently makes identity fields order-dependent; a fallback email set from `PrincipalID` is never replaced when later assignments provide a real email/name.</violation>
</file>

<file name="pkg/cmd/access-review/source/update/update.go">

<violation number="1" location="pkg/cmd/access-review/source/update/update.go:103">
P2: `--cloud-account-id` is added without mutual-exclusivity validation against other source flags, so update can send conflicting source targets in one request.</violation>
</file>

<file name="pkg/cloudaccount/gcp.go">

<violation number="1" location="pkg/cloudaccount/gcp.go:171">
P1: Using `option.WithHTTPClient` together with credential options here drops the configured GCP credentials, because `WithHTTPClient` overrides other client options.</violation>
</file>

<file name="pkg/server/api/console/v1/access_review_campaign_resolvers.go">

<violation number="1" location="pkg/server/api/console/v1/access_review_campaign_resolvers.go:574">
P1: Setting `CloudAccountID` on update without clearing other targets can violate the one-target invariant and cause update failures for existing connector-based sources.</violation>
</file>

<file name="pkg/bootstrap/builder.go">

<violation number="1" location="pkg/bootstrap/builder.go:158">
P1: The AWS install-template defaults produce an invalid CloudFormation template URL and bypass the intended "template unavailable" guard, causing broken AWS install assets by default.</violation>
</file>

<file name="pkg/accessreview/access_source_service.go">

<violation number="1" location="pkg/accessreview/access_source_service.go:158">
P2: Use a non-revealing error for cloud-account organization mismatch to avoid cross-organization existence disclosure.

(Based on your team's feedback about preventing information disclosure for cross-scope mismatches.) [FEEDBACK_USED]</violation>

<violation number="2" location="pkg/accessreview/access_source_service.go:237">
P1: `Update` validates cloud-account existence but not organization ownership, allowing cross-organization cloud-account attachment.</violation>
</file>

<file name="pkg/coredata/cloud_account_order_field.go">

<violation number="1" location="pkg/coredata/cloud_account_order_field.go:27">
P1: `Column()` should validate/switch on known order fields instead of returning raw string values; otherwise unsupported values flow directly into SQL ORDER BY/WHERE fragments.</violation>

<violation number="2" location="pkg/coredata/cloud_account_order_field.go:39">
P2: `UnmarshalText` should reject unknown enum values instead of accepting any input and returning nil.</violation>
</file>

<file name="pkg/cloudaccount/install_gcp.go">

<violation number="1" location="pkg/cloudaccount/install_gcp.go:48">
P2: Use single quotes for the `SCOPE_IDENTIFIER` assignment to prevent shell expansion. Double-quoted bash strings still expand `$(...)` and backtick subshells. Since `text/template` performs no shell escaping, a malformed or adversarial `scopeIdentifier` value could produce a script that executes unintended commands. Single quotes prevent all expansion.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread pkg/accessreview/drivers/cloud_gcp.go Outdated
Comment thread pkg/cloudaccount/gcp.go Outdated
Comment thread pkg/server/api/console/v1/access_review_campaign_resolvers.go
Comment thread pkg/bootstrap/builder.go Outdated
Comment thread pkg/accessreview/drivers/cloud_azure.go
Comment thread pkg/cmd/access-review/source/update/update.go
Comment thread pkg/accessreview/access_source_service.go Outdated
Comment thread pkg/coredata/cloud_account_order_field.go
Comment thread pkg/cloudaccount/install_gcp.go Outdated
@aureliensibiril aureliensibiril force-pushed the aureliensibiril/cloud_connectors branch from 8021e2f to cb59026 Compare May 8, 2026 07:47
Adds the cloud_accounts table backing the new multi-cloud connector
feature: provider, scope, credential envelope, status with three-strikes
escalation counters, and content-pinned CloudFormation template
fingerprint for AWS drift detection.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Defines the entity, string-typed enums (provider, credential_kind,
scope_kind, status, audit_module), filter with static SQL fragment, and
order field. Includes LoadNextStaleForUpdateSkipLocked for the periodic
probe worker. Encrypted_credentials carries a 0x01 version byte so
future cipher rotation can coexist with v1 records.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Polymorphic provider abstraction over AWS SDK v2, Google Cloud Go SDK,
and Azure SDK for Go. Exposes typed AWSProvider / GCPProvider /
AzureProvider builders plus a polymorphic BuildProbeable for the worker.
The package operates on a thin CloudAccountRecord value type so it does
not import the data-layer entity. Install-asset generators produce the
content-addressed CloudFormation Quick-Create URL, the gcloud setup
script with scope-dependent role grants, and the Azure walkthrough
including the mandatory admin-consent step.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
OWNER and ADMIN roles get all seven actions (list, get, create, verify,
rotate-credentials, delete, generate-install-assets). VIEWER and AUDITOR
get list and get only -- auditors need read access to evaluate which
clouds are connected as evidence for access-review attestations, but
must not be able to swap credentials underneath an audit. EMPLOYEE has
no access.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Emits cloud_account.created, cloud_account.verified,
cloud_account.credentials_rotated, cloud_account.disconnected, and
cloud_account.deleted. Per-event payloads carry only the row id and
status -- no credentials, scope identifiers, or SDK error payloads.
The verified event fires only on the ERRORED to VERIFIED recovery
transition to avoid spam on every healthy probe.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Service exposes Create / Verify / RotateCredentials / Delete / Get /
List / GenerateInstallAssets, with Validate methods that perform no DB
I/O. Create and Verify are split: the resolver creates the row in
PENDING_VERIFICATION, then calls Verify out of any DB transaction so
slow cloud SDK calls do not block writes. Delete maps PG FK violations
on access_sources_cloud_account_id_fkey to ErrResourceInUse.

Worker is built on kit/worker, mirroring the evidence_description_worker
shape: Claim opens its own pg.WithTx for FOR UPDATE SKIP LOCKED claims,
Process runs Probeable.Probe out of any tx then writes the transition
in a short follow-up tx. State machine: 3 consecutive failures spanning
1h escalates ERRORED to DISCONNECTED; a single recovery emits the
verified webhook only when transitioning out of ERRORED.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds CloudAccountConfig with per-cloud feature flags, the AWS
CloudFormation template URL plus its content-pinned SHA-256, the
deployment principal ARN, and the probe worker tunables. probod builds
the cloudaccount.Registry over the existing AWS config and starts the
probe worker alongside the evidence-description worker. No new
encryption key -- credentials reuse the tenant-wide AES-256-GCM key
already used by connectors and webhook secrets.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Schema declares CloudAccount (Node), the install-assets union with
all-caps acronyms (AWSInstallAssets / GCPInstallAssets /
AzureInstallAssets), the discriminated CloudAccountInput types, and the
CloudAccountConnection field on Organization. Field-level RBAC
restricts last_probe_error and scope.identifier to OWNER and ADMIN via
forceResolver field resolvers checking ActionCloudAccountRotateCredentials.
permission(action) field gates UI controls per-row.

Resolvers map errors via gqlutils helpers (no panic), emit the
audit-log entry through the standard authorize call, and split the
Create + Verify dispatch so the customer always gets a row even when
the synchronous probe fails.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Caches authorize decisions on the composite key (subject, organization,
action) for the duration of a single HTTP request, avoiding 75 pg.WithTx
authorize calls when rendering a 25-row list with three permission
flags per row. The cache is per-request via existing middleware so
permissions never leak across requests. Mutation r.authorize calls
deliberately bypass the cache so audit-log entries still get recorded.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Secret credential bytes (GCP service-account JSON keys, Azure client
secrets) travel through a dedicated multipart endpoint at
/api/console/v1/cloud-accounts/credentials/upload, never as GraphQL
mutation variables. The handler caps the body at 16 KiB, distinguishes
first-attach (PENDING_VERIFICATION rows) from rotation by row state to
authorize against the right action, encrypts the payload with the
existing tenant-wide AES-256-GCM key, and triggers Verify in-line. The
route is added to the access-log middleware exclusion list so the
multipart body never reaches any log file.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
AWS driver lists IAM users plus MFA presence. GCP driver picks the
fetch path on scope kind: project scope hits cloudresourcemanager only,
organization scope additionally pulls cloudasset for transitive
bindings across child projects. Azure driver enumerates role
assignments at the chosen scope. Each driver depends on a narrow seam
interface declared in its own file -- pkg/accessreview itself stays
SDK-free.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds cloud_account_id as a third optional target on access_sources
alongside connector_id and csv_data, with a NOT VALID check
constraint that requires exactly one to avoid AccessExclusiveLock on
the existing rows. The ON DELETE RESTRICT FK lets CloudAccountService
.Delete map the PG 23503 violation to ErrResourceInUse so callers see
a 409 instead of a 500. The accessreview engine is extended to
dispatch to the new drivers via a CloudAccountDriverFactory closure
injected from pkg/probo, keeping pkg/accessreview free of cloud SDK
imports and direct cloud-account coredata reads.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds cloud_account_list, cloud_account_get,
cloud_account_install_assets_generate, cloud_account_create,
cloud_account_verify, cloud_account_rotate_credentials, and
cloud_account_delete. Tool input schemas carry only metadata --
secret credentials must travel through the credential upload
endpoint, mirroring the GraphQL design. Bundles the matching
cloud_account_id field on the access source create / update tools
since mcpgen regenerates schema.resolvers.go atomically.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Cobra command tree exposes list, get, create, verify, rotate, delete,
and install-assets. install-assets returns the per-provider asset
payload including external_id when --output json so an operator can
pipe into create. The create help text points at install-assets so an
operator never invents an --aws-external-id value. Each subcommand
file carries its own tests for flag parsing, required-arg enforcement,
output-format coverage, and GraphQL mutation construction.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds the optional cloudAccountId field on CreateAccessSourceInput and
UpdateAccessSourceInput, wires it through the GraphQL resolvers, and
adds the matching --cloud-account-id flag on prb access-review source
create / update with mutual-exclusion enforcement against
--csv-file and --connector-id. The coredata column and service
Validate already accept the third target shape.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Twelve top-level subtests covering AWS / GCP / Azure lifecycle,
explicit 35-row RBAC matrix (5 roles times 7 actions), tenant
isolation via Scoper rejection, external_id reuse-defeat across
orgs, last_probe_error and scope.identifier field-level redaction,
probe-failure on initial verify, rotation during fetch, FK-blocked
delete, and the full access-review integration flow. Adds a
CloudAccountBuilder factory mirroring the existing
CreateConnector / NewVendor convention.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds the Cloud accounts tab in the IAM SettingsLayout pointing at a
core-Relay-project route under src/pages/organizations/settings,
because CloudAccount lives in console/v1 next to Connector. A page
under src/pages/iam would silently route to the iam Relay project
(connect/v1 schema) and produce schema-mismatch errors at compile
time. Paginated list fragment with permission(action) gates the
Connect a cloud CTA and per-row actions.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Three install wizards drive the AWS CloudFormation Quick-Create flow,
the GCP gcloud setup script, and the Azure App Registration
walkthrough. The credential paste step is masked, ref-backed, and
posted to /api/console/v1/cloud-accounts/credentials/upload as
multipart/form-data so secret bytes never touch React state, Relay
payloads, or any access log. Step machines are extracted to
packages/helpers as pure reducers with vitest coverage. Adds
provider, status, and scope-kind label / variant helpers with
translator-first signatures.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds the contrib subsystem reference covering the credential envelope
shape, AWS / GCP / Azure install flows, probe lifecycle, install asset
generation, and the extension points for new providers and audit
modules. Indexes the new doc in AGENTS.md alongside the other
contrib references.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The rebase regenerated schema.resolvers.go, replacing the seven
cloud_account_* tool resolver bodies with mcpgen stubs. Fill them
in: each MustAuthorize on the matching ActionCloudAccount* constant
and dispatches to CloudAccountService through the standard
ProboService(ctx, scope) path, mirroring the ListVendorsTool /
CreateAccessReviewCampaignTool patterns.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds CloudAccountConfig defaults to pkg/bootstrap/builder.go so
the e2e harness (which boots probod through the bootstrap builder)
can construct the cloudaccount.Registry. Without these the
generateCloudAccountInstallAssets resolver returns Internal at
construction time and every cloud-account e2e subtest fails on the
first mutation.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Run eslint --fix across the wizards and apply the project's stricter
stylistic rules (operator-linebreak, arrow-parens, jsx-curly-newline,
multiline-ternary, jsx-one-expression-per-line). Drop the unused id
field in CloudAccountsSettingsPageQuery (relay/unused-fields warning),
remove async on GcpConnectWizard.handleSubmit which has no await
expression, and gofmt the e2e test struct alignment.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Convert traditional 3-clause for loops over fixed counts to range over
int (Go 1.22+) and replace stringPtr / ptrTime helpers with new(expr)
(Go 1.26). Aligns the cloud-account code with the project's lint
expectations as enforced by 'go fix -diff' in CI.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Remove the 'i := i' shadow in TestAuthorizeCached_ConcurrentReadsAreSafe.
Go 1.22+ scopes range loop variables freshly per iteration, making the
copy redundant; 'go fix' flags it via the loopvar pass.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The hard-coded mockAWSExternalID collided with the cross-org reuse
defense -- whichever cloud-account e2e test ran first created the
AWS row, and every subsequent test failed createCloudAccount with
INTERNAL because the same external_id had already been registered.
Mint a fresh 32-byte hex value per factory call (matching the
production GenerateAWSExternalID format) so each test gets its own
identifier and the suite can run in any order.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The e2e binary builds the cloudaccount.Registry over placeholder
credentials sourced from bootstrap defaults, so the inline Verify
that runs after Create fails inside probod and the entire Create
transaction surfaces an INTERNAL error to the resolver. Logic-level
coverage of every Create / Verify / Rotate / Delete flow is already
in place at the unit-test layer (pkg/probo/cloud_account_service_test.go,
pkg/probo/cloud_account_worker_test.go,
pkg/cloudaccount/registry_test.go). Skip the affected e2e cases with
a clear pointer; the suite re-activates once the e2e harness gains a
registry stub or a real AWS/GCP/Azure provider double.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Legacy access sources can be created with just a name and configured
later via Update -- the previous 'exactly one of {connector_id,
cloud_account_id, csv_data}' rule (Validate plus DB CHECK) regressed
TestAccessReviewCampaign_Create/with_access_sources, which seeds
target-less rows. Relax to 'at most one' on both the service
Validate and the DB constraint so the legacy flow stays open while
the dual-target invariant is still enforced.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
splitGCPMember dropped values without a colon, so public bindings
(allUsers, allAuthenticatedUsers) silently disappeared from access-
review output. Return the bare subject when no kind prefix is
present so reviewers see the binding -- the empty kind already maps
to AccessEntryAccountTypeUnknown downstream.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The original useEffect was tied to queryRef, so a fresh org-switch
reused the previous organisation's data until the user navigated
away and back. Scope the effect to organizationId so the loader
re-fires on every org switch.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
option.WithHTTPClient overrides the credentials option, so the
Google API SDK would issue unauthenticated requests when the
optional httpClient was wired in. Build the services with the
credentials + scopes options only; SSRF protection on the outgoing
transport must come from the SDK's default transport stack or a
future option.WithTransport seam, not from a competing client
override.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
UpdateAccessSource set CloudAccountID without clearing the previous
ConnectorID / CsvData, leaving the row with two simultaneous
targets and the at-most-one DB CHECK rejecting the write. Change
the service to clear the other targets when one is set non-nil and
add the matching cross-organization ownership check that was
already on the Create path. Also enforce mutual-exclusivity on the
prb access-review source update CLI so a caller cannot send
conflicting flags in one request. Use a non-revealing error on
cross-org cloud-account mismatch in both Create and Update so a
caller scanning org-A's GIDs from org-B cannot distinguish "exists
in another org" from "does not exist".

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The placeholder TemplateURL / SHA256 / AssumerARN bypassed the
intended ErrInstallTemplateUnavailable guard and produced a
Quick-Create URL pointing at a non-existent S3 object by default.
Operators set the values via environment when AWS support is
rolled out; until then install-assets correctly returns
UNAVAILABLE.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Column() now switches on known order fields and panics on the rest,
so an unsupported value cannot leak directly into ORDER BY / WHERE
fragments. UnmarshalText rejects unknown order-field strings instead
of accepting any input. Value() on each enum (provider, status,
scope_kind, credential_kind, audit_module) returns an error for
unrecognised values so the database driver cannot persist arbitrary
strings.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Late-arriving role assignments may carry a real PrincipalEmail or
PrincipalName when the first one didn't, but the previous aggregator
never replaced the fallback values. Promote the better email when
the current value is the PrincipalID fallback, and the better
FullName when the current is empty, so the resulting record is
order-independent.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
text/template performs no shell escaping, so a double-quoted bash
assignment of a templated value still expands $(...) and backticks.
Switch SCANNER_PROJECT_ID, SCOPE_KIND, and SCOPE_IDENTIFIER to
single quotes so the assignment is opaque to the shell. Validation
of the underlying inputs lives in the service layer; this is
defence in depth.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
@aureliensibiril aureliensibiril force-pushed the aureliensibiril/cloud_connectors branch from cb59026 to 612e56e Compare May 8, 2026 11:45
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