Skip to content

[CXH-1585] feat: account provisioning + enable/disable user actions via Retool REST API#36

Open
sergiocorral-conductorone wants to merge 8 commits into
mainfrom
sergiocorral/cxh-1585-retool-add-account-provisioning-and-deprovisioning
Open

[CXH-1585] feat: account provisioning + enable/disable user actions via Retool REST API#36
sergiocorral-conductorone wants to merge 8 commits into
mainfrom
sergiocorral/cxh-1585-retool-add-account-provisioning-and-deprovisioning

Conversation

@sergiocorral-conductorone

@sergiocorral-conductorone sergiocorral-conductorone commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Adds user account provisioning and reversible enable/disable actions to baton-retool (CXH-1585). This augments the existing Postgres-only connector with a second auth surface (the Retool REST API) used only for the account lifecycle. Sync and the existing group/page provisioning stay 100% on Postgres and are unchanged.

Changes

  • config — new retool-api-base-url + retool-api-token (declared FieldsRequiredTogether, both optional, token is WithIsSecret). Sync-only deployments keep working without them.
  • client — optional bearer-auth REST client (uhttp) alongside the pgx pool, a single doRequest chokepoint, and CreateUser / GetUserByEmail / SetUserActive + ValidateREST against /api/v2/users. GetUserSID resolves the synced user:<int64> (legacy_id) to the REST sid via Postgres — no mutable-email join.
  • connectorAccountCreationSchema in Metadata(), a REST probe in Validate() (only when configured), and enable_user / disable_user global actions (ACTION_TYPE_ACCOUNT_ENABLE/ACCOUNT_DISABLE, modeled on baton-aws-cognito) backed by PATCH /api/v2/users/{id} on /active.
  • usersCreateAccount (returns the durable user:<legacy_id> straight from the create response, idempotent on duplicate email) and CreateAccountCapabilityDetails.
  • sdk — upgraded baton-sdk v0.2.66 → v0.13.0 (one signature fix + the new *v2.LocalCredentialOptions CreateAccount param; unlocks WithIsSecret and the action framework).
  • ci — the Docker-based integration test now runs on every PR in ci.yaml (Retool stack + sync + grant/revoke + account lifecycle); golangci-lint migrated to v2 (v1 cannot target Go 1.25).
  • docs — README, public connector.mdx, internal docs/docs-info.md, plus the previously missing SELECT ("sid") grant for the dedicated baton Postgres user.

Retool API semantics (verified against a live self-hosted instance)

  • The REST base is /api/v2 (not /api/v1).
  • Retool has no hard delete: DELETE /api/v2/users/{id} returns 204 but only sets active=false (row persists, memberships kept). Exposing that as an account Delete would misrepresent a reversible deactivation as a removal, so the connector models deprovisioning as the disable_user action, reversible via enable_user (PATCH with {"operations":[{"op":"replace","path":"/active","value":<bool>}]}). Both are idempotent.
  • The connector resolves legacy_id → sid via the Postgres pool it already holds, sidestepping the int64↔UUID mismatch entirely.

Testing

Verified end-to-end against a self-hosted Retool 3.334.17:

  • sync (baseline) and group grant/revoke (existing path, unchanged) — pass.
  • Account lifecycle: create → verify active → disable_user → verify deactivated → enable_user → verify reactivated → duplicate disable (idempotency) — pass.
  • Negative paths (unknown user_id, missing argument) fail cleanly with NotFound/InvalidArgument.
  • capabilities advertises CAPABILITY_ACTIONS + CAPABILITY_ACCOUNT_PROVISIONING (no resource delete).
  • Tenant restored to baseline after each run.

The same lifecycle runs in CI on every push (integration-test job in ci.yaml): it stands up a throwaway self-hosted Retool in Docker, seeds an admin, mints a REST token via SQL, and exercises sync + grant/revoke + the account lifecycle. go build, go vet, and golangci-lint (v2) are clean.

🤖 Generated with Claude Code

…H-1585)

Augment the Postgres-only connector with a second auth surface (REST) for
the user account lifecycle. Sync and existing group/page provisioning stay
100% on Postgres and are unchanged.

- config: add retool-api-base-url + retool-api-token (FieldsRequiredTogether,
  optional — sync-only deployments keep working without them).
- client: add an optional bearer-auth REST client (uhttp) alongside the pgx
  pool, a doRequest chokepoint, and CreateUser/GetUserByEmail/DeleteUser +
  ValidateREST against /api/v2/users. user.go gains GetUserSID to resolve the
  synced user:<int64> (legacy_id) to the REST sid via Postgres — no mutable
  email join.
- connector: AccountCreationSchema in Metadata(), REST probe in Validate()
  (only when configured), New() threads the REST config through.
- users: CreateAccount (returns the durable user:<legacy_id> from the create
  response), CreateAccountCapabilityDetails, and Delete. Delete tolerates 404
  and 422 ("already disabled") as success.

Notes on Retool semantics (verified against a live instance):
- The REST base is /api/v2 (not /api/v1).
- DELETE is a soft deactivation (sets enabled=false, keeps memberships,
  reversible); Retool has no hard delete.
- Enable/disable as a connector Action is deferred — the connector's SDK
  version has no action framework.

Docs: README, public connector.mdx (capabilities + REST-token setup +
self-hosted env vars), and new internal docs/docs-info.md updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sergiocorral-conductorone sergiocorral-conductorone requested a review from a team June 9, 2026 21:37
@linear-code

linear-code Bot commented Jun 9, 2026

Copy link
Copy Markdown

CXH-1585

Comment thread cmd/baton-retool/config.go
Comment thread pkg/client/rest_user.go Outdated
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Connector PR Review: [CXH-1585] feat: account provisioning + enable/disable user actions via Retool REST API

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Review mode: incremental since 0b5f60e
View review run

Review Summary

This commit (1308d9f) addresses the prior review feedback. The earlier suggestions are all now resolved: WithDisplayName was added to both config fields, WithIsSecret(true) replaces WithHidden for the token, WithConnectorDisplayName/WithIconUrl/WithHelpUrl were added to the configuration, the REST client now uses uhttp.NewClient with a logger, doRequest surfaces rate-limit annotations, the connector DisplayName is now Retool (title-case), error messages carry the baton-retool: prefix, and the user_id action argument uses a ResourceIdField. The account Delete path was reworked into reversible enable_user/disable_user global actions (PATCH /active), with docs and the integration test updated to match. Dependency bumps (baton-sdk v0.13.0, grpc, go 1.25.2) match the new action framework and LocalCredentialOptions usage. No new issues found. Note: the incremental artifact was partial (vendored/lockfile paths dropped); the full PR diff was scanned for security and correctness.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found.

baton-retool can't be exercised with a simple mock — sync + group/page
provisioning need Retool's internal Postgres, and account provisioning needs
the Retool REST API. This adds a self-contained integration test that stands
up a throwaway self-hosted Retool (postgres + jobs-runner + api) via Docker,
seeds an admin/org, mints a REST token, and runs sync + group grant/revoke +
account provisioning (create -> delete -> dup-delete) against it.

- test/integration/compose.yaml  — minimal 3-service Retool stack.
- test/integration/setup.sh       — wait for health, idempotent admin signup,
  mint a users:read+users:write PAT via SQL (hashedKey = sha256(token)).
- test/integration/provisioning-test.sh — account create/delete/dup-delete +
  assertions via the REST API.
- .github/workflows/integration-test.yaml — schedule + workflow_dispatch
  (heavy: image pull + DB migrations, so not per-PR).

Account-provisioning script verified end-to-end against a live local Retool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found.

- client: doRequest now returns the HTTP status code instead of the raw
  *http.Response — fixes the bodyclose lint failures (uhttp already drains and
  closes the body) and stops the response escaping the chokepoint. Callers
  branch on the status code.
- client: build the request URL with url.URL.JoinPath so a base-URL path
  prefix (e.g. a reverse proxy at /retool) is preserved instead of dropped.
- client: convert CreateUserParams to createUserRequest directly (gosimple).
- config: mark retool-api-token WithHidden(true) so the bearer token stays out
  of --help output and config dumps (this SDK predates WithIsSecret).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found.

@sergiocorral-conductorone sergiocorral-conductorone changed the title feat: add account provisioning/deprovisioning via Retool REST API (CXH-1585) [CXH-1585] feat: add account provisioning/deprovisioning via Retool REST API Jun 10, 2026
- field.NewConfiguration now takes relationship constraints via
  field.WithConstraints(...)
- retool-api-token is declared field.WithIsSecret(true) (the WithHidden
  workaround predated the option)
- CreateAccount takes *v2.LocalCredentialOptions — v0.13.0 rejects the old
  AccountManager interface at startup, not at compile time
- drop the Unimplemented Create() stub: ResourceDeleter is standalone now,
  Delete alone no longer requires the full ResourceManager pair

Verified live against a self-hosted Retool 3.334.17: full sync, account
create -> deactivate -> idempotent duplicate-delete, group grant/revoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fold the docker-based integration job into ci.yaml (the standalone
integration-test.yaml never ran: schedule/workflow_dispatch require the
file on the default branch). Local connectors baton-mysql and baton-ldap
already run their service in Docker on every PR. A concurrency group
cancels superseded runs on new pushes.

Also pin Go via go-version-file: go.mod everywhere (the SDK bump moves
the directive to 1.25.2; main.yaml hardcoded 1.24.x).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
golangci-lint v1.64.8 (the newest v1, built with go1.24) refuses to target
the go 1.25.2 directive the SDK bump requires. Migrate .golangci.yml to the
v2 format (golangci-lint migrate; dropped long-invalid deadcode/varcheck
entries first), move both workflows to golangci-lint-action@v9 with a
pinned v2.11.1, and fix the 8 staticcheck QF1012 findings the v2 linter
surfaces (sb.WriteString(fmt.Sprintf(...)) -> fmt.Fprintf(sb, ...)).

Local `make lint` works again too — the v2 binary previously couldn't read
the v1-format config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retool's REST API has no hard delete — verified live: DELETE
/api/v2/users/{id} returns 204 but only sets active=false. Exposing that
as an account Delete misrepresents a reversible deactivation as a
removal, so:

- drop userSyncer.Delete and the REST DeleteUser call
- add enable_user/disable_user global actions
  (ACTION_TYPE_ACCOUNT_ENABLE/DISABLE) backed by PATCH /api/v2/users/{id}
  {"operations":[{"op":"replace","path":"/active","value":<bool>}]},
  reusing the legacy_id -> sid Postgres resolution
- CreateAccount stays unchanged
- rewrite the integration lifecycle test: create -> disable -> enable ->
  duplicate-disable (idempotent), via --invoke-action
- docs: no-hard-delete note, actions, and add the missing SELECT ("sid")
  grant for the dedicated baton Postgres user (sid resolution needs it)

Verified end-to-end against a self-hosted Retool 3.334.17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sergiocorral-conductorone sergiocorral-conductorone changed the title [CXH-1585] feat: add account provisioning/deprovisioning via Retool REST API [CXH-1585] feat: account provisioning + enable/disable user actions via Retool REST API Jun 11, 2026
Comment thread cmd/baton-retool/config.go
Comment thread cmd/baton-retool/config.go
Comment thread cmd/baton-retool/config.go Outdated
Comment thread pkg/client/client.go Outdated
Comment thread pkg/client/rest_user.go Outdated
Comment thread pkg/connector/connector.go Outdated
Comment thread pkg/connector/connector.go
Comment thread pkg/connector/users.go Outdated
Comment thread pkg/connector/actions.go Outdated
Comment thread pkg/connector/actions.go Outdated
- config: add WithDisplayName to the REST fields; add connector display
  name, icon URL, and help URL to the configuration
- connector: title-case Metadata DisplayName ("Retool"); prefix errors
  with "baton-retool:"; surface REST rate-limit annotations from Validate
- client: build the REST client via uhttp.NewClient (WithLogger +
  WithTimeout) then wrap it, so HTTP request/response debug logs fire
  (the raw http.Client bypassed the logging transport); thread rate-limit
  annotations through doRequest and all REST callers
- users: prefix errors; carry rate-limit annotations through CreateAccount
- actions: switch the enable_user/disable_user user_id argument from a
  free-form string to a ResourceIdField constrained to the user resource
  type (C1 renders a resource picker); extract it via
  actions.RequireResourceIDArg with a resource-type guard; prefix errors;
  thread annotations
- test: pass user_id as a {resource_type_id, resource_id} struct to match
  the new ResourceIdField arg shape

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found.

@btipling btipling removed their assignment Jun 24, 2026
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.

5 participants