Skip to content
Merged
65 changes: 50 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# baton-retool
`baton-retool` is a connector for [Retool](https://retool.com/) built using the [Baton SDK](https://github.com/conductorone/baton-sdk). It connects directly to your primary Retool Postgres database and syncs data about users, groups, organizations, pages, and resources.
`baton-retool` is a connector for [Retool](https://retool.com/) built using the [Baton SDK](https://github.com/conductorone/baton-sdk). It connects directly to your primary Retool Postgres database and syncs data about users, groups, organizations, pages, and resources. It can also provision and deprovision Retool user accounts via the Retool REST API.

Check out [Baton](https://github.com/conductorone/baton) to learn more the project in general.

## Capabilities

| Capability | Status |
|------------|--------|
| Sync (users, groups, organizations, pages, resources) | Yes |
| Provisioning (Grant/Revoke) | Yes — group membership and page access |
| Account Creation | Yes — via the Retool REST API |
| Account Deletion | Yes — via the Retool REST API (deactivates the account; see note below) |
| Event Feeds | No |

> **Note on account deletion:** Retool's REST API has no hard delete. Deprovisioning **deactivates** the user (blocks sign-in) and keeps their group memberships; the account can be reactivated in Retool. Account provisioning requires the `retool-api-base-url` and `retool-api-token` settings; sync and group/page provisioning need only the `connection-string`.

# Getting Started

## Setup
Expand Down Expand Up @@ -32,6 +44,27 @@ GRANT DELETE ON user_groups TO baton;
BATON_CONNECTION_STRING="user=baton password=baton host=localhost port=5432 dbname=hammerhead_production" baton-retool
```

4. (Optional) To enable account provisioning/deprovisioning, create a Retool API token (**Settings → API**) with the `users:read` and `users:write` scopes, then also set the REST configuration. Both fields are required together; omit them for sync-only deployments.
```bash
BATON_CONNECTION_STRING="user=baton password=baton host=localhost port=5432 dbname=hammerhead_production" \
BATON_RETOOL_API_BASE_URL="https://your-org.retool.com" \
BATON_RETOOL_API_TOKEN="retool_xxxxxxxx" \
baton-retool --provisioning
```

## Configuration

| Flag | Env Var | Required | Description |
|------|---------|----------|-------------|
| `--connection-string` | `BATON_CONNECTION_STRING` | Yes | Postgres DSN for the Retool database |
| `--retool-api-base-url` | `BATON_RETOOL_API_BASE_URL` | No* | Retool REST base URL (e.g. `https://your-org.retool.com`) — required for account provisioning |
| `--retool-api-token` | `BATON_RETOOL_API_TOKEN` | No* | Retool API token (`users:read` + `users:write`) — required for account provisioning |
| `--skip-pages` | `BATON_SKIP_PAGES` | No | Skip syncing pages |
| `--skip-resources` | `BATON_SKIP_RESOURCES` | No | Skip syncing resources |
| `--skip-disabled-users` | `BATON_SKIP_DISABLED_USERS` | No | Skip syncing disabled users |

\* `retool-api-base-url` and `retool-api-token` are required together (both or neither).

## brew

```
Expand Down Expand Up @@ -87,20 +120,22 @@ Available Commands:
help Help about any command

Flags:
--client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)
--client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)
--connection-string string required: The connection string for connecting to retool database ($BATON_CONNECTION_STRING)
-f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z")
-h, --help help for baton-retool
--log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json")
--log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info")
-p, --provisioning This must be set in order for provisioning actions to be enabled ($BATON_PROVISIONING)
--skip-disabled-users Skip syncing disabled users ($BATON_SKIP_DISABLED_USERS)
--skip-full-sync This must be set to skip a full sync ($BATON_SKIP_FULL_SYNC)
--skip-pages Skip syncing pages ($BATON_SKIP_PAGES)
--skip-resources Skip syncing resources ($BATON_SKIP_RESOURCES)
--ticketing This must be set to enable ticketing support ($BATON_TICKETING)
-v, --version version for baton-retool
--client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)
--client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)
--connection-string string required: The connection string for connecting to retool database ($BATON_CONNECTION_STRING)
-f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z")
-h, --help help for baton-retool
--log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json")
--log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info")
-p, --provisioning This must be set in order for provisioning actions to be enabled ($BATON_PROVISIONING)
--retool-api-base-url string Base URL of the Retool REST API, e.g. https://<org>.retool.com. Required only for account provisioning/deprovisioning. ($BATON_RETOOL_API_BASE_URL)
--retool-api-token string Retool API token with users:read + users:write. Required only for account provisioning/deprovisioning. ($BATON_RETOOL_API_TOKEN)
--skip-disabled-users Skip syncing disabled users ($BATON_SKIP_DISABLED_USERS)
--skip-full-sync This must be set to skip a full sync ($BATON_SKIP_FULL_SYNC)
--skip-pages Skip syncing pages ($BATON_SKIP_PAGES)
--skip-resources Skip syncing resources ($BATON_SKIP_RESOURCES)
--ticketing This must be set to enable ticketing support ($BATON_TICKETING)
-v, --version version for baton-retool

Use "baton-retool [command] --help" for more information about a command.
```
18 changes: 17 additions & 1 deletion cmd/baton-retool/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,31 @@ var (
"skip-disabled-users",
field.WithDescription("Skip syncing disabled users"),
)
// REST surface for account provisioning/deprovisioning (CXH-1585). Optional:
// sync-only deployments keep working without these; the lifecycle handlers fail
// fast with a clear error when they are absent.
RetoolAPIBaseURL = field.StringField(
Comment thread
sergiocorral-conductorone marked this conversation as resolved.
"retool-api-base-url",
field.WithDescription("Base URL of the Retool REST API, e.g. https://<org>.retool.com. Required only for account provisioning/deprovisioning."),
)
RetoolAPIToken = field.StringField(
Comment thread
sergiocorral-conductorone marked this conversation as resolved.
"retool-api-token",
field.WithDescription("Retool API token with users:read + users:write. Required only for account provisioning/deprovisioning."),
)
Comment thread
sergiocorral-conductorone marked this conversation as resolved.
)

var configurationFields = []field.SchemaField{
ConnectionString,
SkipPages,
SkipResources,
SkipDisabledUsers,
RetoolAPIBaseURL,
RetoolAPIToken,
}

var configRelations = []field.SchemaFieldRelationship{}
// retool-api-base-url and retool-api-token are both-or-neither.
var configRelations = []field.SchemaFieldRelationship{
field.FieldsRequiredTogether(RetoolAPIBaseURL, RetoolAPIToken),
}

var configuration = field.NewConfiguration(configurationFields, configRelations...)
4 changes: 3 additions & 1 deletion cmd/baton-retool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e
skipPages := v.GetBool(SkipPages.FieldName)
skipResources := v.GetBool(SkipResources.FieldName)
skipDisabledUsers := v.GetBool(SkipDisabledUsers.FieldName)
apiBaseURL := v.GetString(RetoolAPIBaseURL.FieldName)
apiToken := v.GetString(RetoolAPIToken.FieldName)

cb, err := connector.New(ctx, connString, skipPages, skipResources, skipDisabledUsers)
cb, err := connector.New(ctx, connString, skipPages, skipResources, skipDisabledUsers, apiBaseURL, apiToken)
if err != nil {
l.Error("error creating connector builder", zap.Error(err))
return nil, err
Expand Down
54 changes: 47 additions & 7 deletions docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@ description: C1 provides identity governance and just-in-time provisioning for R
sidebarTitle: Retool
---

{/* AUTO-GENERATED:START - capabilities
Generated from baton_capabilities.json. Do not edit manually. */}

## Capabilities

| Resource | Sync | Provision |
The Retool connector syncs the following resources:

| Resource | Sync | Provision |
| :--- | :--- | :--- |
| Accounts | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | |
| Groups | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | <Icon icon="square-check" iconType="solid" color="#c937ae"/> |
| Organizations | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | |
| Pages (Apps) | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | <Icon icon="square-check" iconType="solid" color="#c937ae"/> |
| Resources | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | |
| Accounts | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | <Icon icon="square-check" iconType="solid" color="#c937ae"/> Create, Delete |
| Groups | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | <Icon icon="square-check" iconType="solid" color="#c937ae"/> Grant, Revoke |
| Organizations | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | |
| Pages (Apps) | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | <Icon icon="square-check" iconType="solid" color="#c937ae"/> Grant, Revoke |
| Resources | <Icon icon="square-check" iconType="solid" color="#c937ae"/> | |

{/* AUTO-GENERATED:END - capabilities */}

**Notes:**
- Sync and group/page provisioning use a direct connection to the Retool Postgres database (the `connection-string` setting). Account provisioning (Create / Delete) additionally uses the Retool REST API and requires the `retool-api-base-url` and `retool-api-token` settings — sync-only deployments work without them.
- Deprovisioning (Delete) **deactivates** the account (blocks sign-in) rather than permanently deleting it. The user is retained with its group memberships and can be reactivated in Retool; Retool's API does not expose a hard delete.

## Gather Retool credentials

Expand Down Expand Up @@ -56,6 +67,27 @@ Compose and save the Retool connection string you'll use when setting up the con
`"user=baton password=secure-password host=localhost port=5432 dbname=hammerhead_production"`
</Step>
</Steps>

### (Optional) Create a Retool API token for account provisioning

Account creation and deprovisioning use the Retool REST API. Skip this section if you only need sync and group/page provisioning.

<Warning>
A Retool admin must perform this task, and your Retool plan must include REST API access.
</Warning>

<Steps>
<Step>
In Retool, go to **Settings** > **API** and create a new API token.
</Step>
<Step>
Grant the token the **`users:read`** and **`users:write`** scopes (both are required to create, look up, and deprovision users).
</Step>
<Step>
Copy and save the token, and note your Retool base URL (for example `https://<your-org>.retool.com`). You'll provide both when configuring the connector.
</Step>
</Steps>

**That's it!** Next, move on to the connector configuration instructions.

## Configure the Retool connector
Expand Down Expand Up @@ -139,6 +171,10 @@ stringData:
# Retool credentials
BATON_CONNECTION_STRING: <The Retool connection string, in format "user=baton password=secure-password host=localhost port=5432 dbname=hammerhead_production">

# Optional: required only for account provisioning/deprovisioning (REST API)
BATON_RETOOL_API_BASE_URL: <Your Retool base URL, e.g. https://your-org.retool.com>
BATON_RETOOL_API_TOKEN: <Retool API token with users:read + users:write scopes>

# Optional: include if you want C1 to provision access using this connector
BATON_PROVISIONING: true
```
Expand Down Expand Up @@ -190,4 +226,8 @@ Check that the connector data uploaded correctly. In C1, click **Apps**. On the
</Steps>
**That's it!** Your Retool connector is now pulling access data into C1.
</Tab>
</Tabs>
</Tabs>

<Tip>
You can download the latest `baton-retool` release binaries from the [ConductorOne download center](https://dist.conductorone.com/ConductorOne/baton-retool).
</Tip>
61 changes: 61 additions & 0 deletions docs/docs-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Baton Retool - Connector Documentation

This document provides information needed to set up and use the connector.

## Connector Capabilities

### 1. What resources does the connector sync?

| Resource | Description |
|----------|-------------|
| **Organization** | The Retool org/tenant; parent scope for all other resources. |
| **User** | Retool user accounts (principals) with email, name, and enabled/disabled status. |
| **Group** | Permission groups; users hold membership (and optional group-admin) grants. |
| **Page (App)** | Retool apps; groups are granted an access level (read/write/own). |
| **Resource** | Retool data resources (databases, APIs); synced for visibility. |

### 2. Can the connector provision any resources? If so, which ones?

Yes.

| Resource | Grant | Revoke | Create | Delete |
|----------|-------|--------|--------|--------|
| **Group membership** | ✅ Adds the user to the group (Postgres `user_groups`) | ✅ Removes the user from the group | - | - |
| **Page (App) access** | ✅ Grants the group an access level on the page | ✅ Removes the group's access | - | - |
| **Account (User)** | - | - | ✅ Creates/invites a Retool user via the REST API | ✅ Deprovisions the user via the REST API |

**Important behavioral notes:**
- **Sync and group/page provisioning** run entirely against the Retool **Postgres database** (the `connection-string`). They do not require the REST API.
- **Account Create/Delete** use the Retool **REST API** (`/api/v2/users`) and require `retool-api-base-url` + `retool-api-token`. These are optional config; when absent, the account-lifecycle handlers fail fast with a clear "REST API not configured" error while sync and grant/revoke keep working.
- **Account "Delete" is a soft deactivation, not a hard delete.** Retool's REST API has no hard-delete endpoint — `DELETE /api/v2/users/{id}` sets the user to disabled (blocks sign-in), retains group memberships, and is reversible. Re-deleting an already-deactivated user and deleting an unknown user are both treated as success (idempotent).
- The connector resolves the synced `user:<int64>` (Postgres `id`, exposed as `legacy_id` over REST) to the REST `sid` (`user_<uuid>`) via a direct Postgres lookup — no email-based matching.
- There is **no enable/disable connector action** (the connector's SDK version predates the action framework); deprovisioning is exposed via account Delete.

## Connector Credentials

### 1. What credentials or information are needed to set up the connector?

| Credential | Required | Description |
|------------|----------|-------------|
| **Connection string** | Yes | Postgres DSN for the Retool database (`user=… password=… host=… port=5432 dbname=hammerhead_production`). Used for sync and group/page provisioning. |
| **Retool API base URL** | No* | Retool base URL, e.g. `https://<org>.retool.com`. Required only for account provisioning. |
| **Retool API token** | No* | Retool API token with `users:read` + `users:write` scopes. Required only for account provisioning. |

\* `retool-api-base-url` and `retool-api-token` are required together (both or neither).

### 2. How are these credentials obtained?

- **Connection string:** Connect to the Retool Postgres database and create a dedicated user (`CREATE USER baton …`) with the SELECT/INSERT/UPDATE/DELETE grants listed in the repo `README.md`, then compose the DSN.
- **API token:** In Retool, go to **Settings → API**, create a token, and grant it the `users:read` and `users:write` scopes. Note the org base URL.

## Additional Notes

### Retool Plan Requirements

- **Direct database access** to Retool's primary Postgres DB is generally a **self-hosted Retool** capability (or a managed/peered database you can reach from where the connector runs). This is required for all sync and group/page provisioning.
- **REST API access** (API tokens) may be gated behind a specific Retool plan/tier. Account provisioning/deprovisioning is only available where the REST API and a `users:read`+`users:write` token are available.

### API Documentation Links

- [Retool REST API reference](https://docs.retool.com/reference/api) — user-management endpoints (`/api/v2/users`).
- [Retool API authentication](https://docs.retool.com/reference/api/authentication) — token scopes.
54 changes: 53 additions & 1 deletion pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,34 @@ package client
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/conductorone/baton-sdk/pkg/uhttp"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/jackc/pgx/v4/pgxpool"
)

type Client struct {
db *pgxpool.Pool

// REST surface for account provisioning/deprovisioning (CXH-1585). nil when the
// connector is configured for sync-only (no API base URL + token).
rest *restClient
}

// restClient holds the bearer-authenticated HTTP surface for the Retool REST API.
type restClient struct {
httpClient *uhttp.BaseHttpClient
baseURL *url.URL
token string
}

// RESTEnabled reports whether the REST surface (account lifecycle) is configured.
func (c *Client) RESTEnabled() bool {
return c.rest != nil
}

func (c *Client) ValidateConnection(ctx context.Context) error {
Expand All @@ -21,7 +42,7 @@ func (c *Client) ValidateConnection(ctx context.Context) error {
return nil
}

func New(ctx context.Context, dsn string) (*Client, error) {
func New(ctx context.Context, dsn string, apiBaseURL string, apiToken string) (*Client, error) {
l := ctxzap.Extract(ctx)

config, err := pgxpool.ParseConfig(dsn)
Expand All @@ -46,5 +67,36 @@ func New(ctx context.Context, dsn string) (*Client, error) {
db: db,
}

// Optional REST surface. Both fields are validated as required-together at the
// config layer, so presence of one implies the other.
if apiBaseURL != "" && apiToken != "" {
rest, err := newRESTClient(ctx, apiBaseURL, apiToken)
if err != nil {
return nil, err
}
c.rest = rest
}

return c, nil
}

func newRESTClient(ctx context.Context, apiBaseURL string, apiToken string) (*restClient, error) {
base, err := url.Parse(strings.TrimRight(apiBaseURL, "/"))
if err != nil {
return nil, fmt.Errorf("invalid retool-api-base-url: %w", err)
}
if base.Scheme == "" || base.Host == "" {
return nil, fmt.Errorf("invalid retool-api-base-url %q: must include scheme and host", apiBaseURL)
}

httpClient, err := uhttp.NewBaseHttpClientWithContext(ctx, &http.Client{Timeout: 30 * time.Second})
Comment thread
sergiocorral-conductorone marked this conversation as resolved.
Outdated
if err != nil {
return nil, err
}

return &restClient{
httpClient: httpClient,
baseURL: base,
token: apiToken,
}, nil
}
Loading
Loading