account/list marks the contract's primary account "main" on the live
API, not "mymain" as the published docs example shows, so matching the
documented spelling reported every account as a non-admin, including
the contract owner. Roles cannot stand in: a main account carries the
same technical/billing values a sub-account can hold. Classify off
"sub" instead, the one value the docs and the API agree on.
The fixture copied the docs example, so the test passed on the same
wrong assumption. Its bodies now mirror a live capture, anonymized: the
main account carries no main_account or allow_gui, sub-accounts add
them plus the access lists, and the primary account's type is "main".
A table test pins both spellings.
A review keys accounts on email plus external ID. Email came only from
account/details, and any failure blanked it while still emitting the
record, so a transient 5xx moved an account to a different key and
surfaced it as one account removed and another added. Only the stable
answers now degrade: UpCloud returns 403 ACCOUNT_FORBIDDEN, not 404,
for an account outside the token's reach, and both keep the list-only
fields. Anything else aborts the run.
A blank username no longer discards every account already collected,
matching the sibling drivers.
Resolve the source name from GET /1.3/account so sources read
"UpCloud <username>" rather than staying generic, and link the
connector to its documentation page.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Each of these providers ships an API-key connection path and a driver, but
no Registration carried a DocumentationURL, so the console rendered no
"how do I get this token" link on the connect dialog. probo.com now has a
page per provider, so point each Registration at it.
Slugs are passed explicitly rather than derived from the enum, per
accessReviewDocsURL's contract, so BETTER_STACK maps to better-stack and
INCIDENT_IO to incident-io.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Registration.ExtraSettings was a single flat list, but the API-key and
client-credentials connect dialogs need different fields whenever a
provider offers both paths, because a different create resolver and a
different driver sits behind each. Replace it with
APIKeyExtraSettings and ClientCredentialsExtraSettings, and split the
GraphQL surface to match so a client cannot render one path's settings
on the other.
This fixes two connectors that could not be connected at all.
1Password declared accountId and region only, which are the
client-credentials shape. The API-key dialog therefore rendered those
two fields, mapAPIKeyExtraSettingToField returned nil for both so
buildExtraFields discarded them, and the SCIM-bridge driver failed on an
empty SCIMBridgeURL. The console already mapped scimBridgeUrl, but no
registration declared that key, so the branch was dead. It now declares
scimBridgeUrl on the API-key path and accountId + region on client
credentials.
Langfuse declared baseUrl as required, but mapAPIKeyExtraSettingToField
had no LANGFUSE case, so buildExtraFields dropped the value the customer
typed and the mutation failed with "langfuseBaseUrl is required". Every
other extra-settings provider had a case. The GraphQL input field, the
settings struct, the probe builder and the driver were all already
correct; only the console mapping was missing.
buildExtraFields now takes the settings list explicitly instead of
reading it off the provider, so each dialog passes its own path's list
and cannot silently iterate the other one.
Register rejects a settings list for a path the provider does not offer,
and an empty or duplicate setting key within one list. A key repeated
across the two lists is allowed: that is how a dual-path provider
declares a setting both dialogs need.
The new resolver tests walk the whole chain the console walks, from the
key a Registration declares through the mutation input field to the
persisted settings struct, so a key renamed on one side and not the
other fails in CI instead of at connect time.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Both now have a page on probo.com, so their provider cards in the Add
Source dialog carry a Documentation link. The slugs match the page
filenames, which is what keeps the link off a 404.
Google Analytics and Square stay unlinked: they are OAuth connectors and
no OAuth connector has a docs page yet, so there is nothing to point at.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Two corrections, both settled from Segment's published OpenAPI document
and their own client code rather than guessed.
The registration claimed the Public API exposes no workspace-name
endpoint on the token's scope. That is wrong: Get Workspace is the API
root, GET /, returning data.workspace.name for the workspace the token
is bound to — the base URL already encodes the US/EU region, so the URL
is the whole request. Without a resolver an organization running a prod
and a staging workspace saw two rows both named "Segment".
The per-user GET /users/{id} is what makes a large workspace exceed the
per-source budget, and it exists only to read permissions[].roleName.
Both endpoints return the same UserV1 schema, on which permissions is
declared but optional, so whether the list populates it is a server
behaviour no specification settles. Rather than assume, the list
response is now decoded for permissions and the per-user request is
issued only when the field is absent. Today Segment omits it — their own
Terraform provider's mock returns /users without permissions and
/users/{id} with them — so behaviour is unchanged; if that ever changes
the extra round trip disappears on its own. An empty-but-present array
is authoritative, meaning a user with no roles, not a missing field.
Page size stays at 200: the 1-1000 range is prose in the pagination
guide, the schema sets no maximum, and the migration guide says 200.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The region is a two-value allow-list the server resolves to an API host,
but it rendered as a free-text field: only PostHog is special-cased in
the API-key dialog, everything else falls through to a generic Field.
Typing "EU1" — the region Segment's own UI shows for the EU workspace —
passed the non-empty check, then failed the mutation, and the dialog's
generic error blamed the API key. It is a select now, so the label no
longer has to spell the accepted values out.
An invite that has already been accepted can still be listed, and the
member and the invite were keyed differently (user ID vs email), so the
same person surfaced as two rows — one active with roles, one inactive
without. Invites for an email already seen among members are dropped.
Per-user permission errors now name the user, and the probe URL builds
its query with url.Values rather than a hand-written string.
The region-to-host mapping is the only API-key setting that derives a
value instead of storing input verbatim, and it had no test; a typo in
either host would only have surfaced as a live 404.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Three defects, all found reviewing the rebased branch.
The connection probe hit /v1alpha/accounts, which any analytics.readonly
grant can call, while the driver's first request is the account's
accessBindings — that additionally needs Administrator on the account
and the manage.users.readonly scope. An Editor connecting, or a user
declining the second scope on Google's granular consent screen, probed
green and then 403'd on every campaign fetch, leaving the source
permanently "Connected" with no rows. The probe now targets the same
accessBindings collection the driver reads.
A single unreadable property aborted the whole account. A property the
token cannot see, or one deleted between the list and the read, threw
away every binding already collected; 49 of 50 readable properties are
still worth reviewing, so 403 and 404 now skip that property. Anything
else still fails the fetch.
Fan-out errors named no resource: the account call, the property list
and each per-property call all returned the same "unexpected status"
string, so a 403 on one subproperty out of forty was unattributable.
Errors now carry the account or property ID.
The cassette gains a subproperty parented to another property (only
reachable through the ancestor filter, so it pins the hierarchy walk
that the filter change claimed) and a property returning 403.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The four connectors landed with the ISC header. contrib/claude/license.md
mandates MIT for every source file, and these were the only ISC files
left in pkg/accessreview/drivers and pkg/connector/provider — the two
"Make license declarations consistently MIT" commits had already cleared
both packages, so this re-opened a closed cleanup.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Two OAuth2 and two API-key connectors:
- Google Analytics (GA4): OAuth2 with both analytics.readonly and
analytics.manage.users.readonly (readonly alone 403s on the accounts
list); v1alpha accessBindings enumerated at account and property level
and merged by email; manual account picker (Pattern 1) with a
per-connection probe and name resolver; distinct from Google Workspace.
- Dotfile: API key in the X-DOTFILE-API-KEY header (Pattern 3); GET
/v1/users (owner/admin, suspended_at) with a static probe.
- Segment (Twilio): Public API token as Bearer with a required Region
setting (US or EU) mapped to the regional host; GET /users plus per-user
GET /users/{id} for roles and /invites for pending members; per-connection
BuildProbeURL.
- Square: OAuth2 (EMPLOYEES_READ) or a personal access token (Pattern 3);
POST /v2/team-members/search returns email/status/is_owner directly, so no
role resolution; custom probe and name resolver.
Google Analytics and Square are confidential OAuth clients, wired into the
bootstrap OAuth provider list and .env.example. Segment carries a required
extra setting, so the console add-source dialog maps region onto its
segmentRegion API-key input; without that mapping the value is silently
dropped and the create is rejected.
Cassette-backed driver tests plus unit tests for the Segment probe URL and
the bootstrap OAuth provider list.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The access review Add Source dialog listed each connector with no
path to its setup documentation. Connectors that have a published
docs page on probo.com now surface a "Documentation" link on the
card, opening the page in a new tab; connectors without a page show
nothing extra.
The link is data-driven from the connector registry: a new
DocumentationURL on the provider Registration, populated for the 12
documented providers via a single accessReviewDocsURL helper, is
surfaced as a nullable documentationUrl on ConnectorProviderInfo and
rendered by the console only when present. This keeps the registry
the single source of truth and adds no client-side provider map.
The links resolve once the probo.com access-review docs pages are
deployed; until then they 404, so deploy the docs alongside this
change.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The connection-status probe and the access-review driver each had
their own copy of the "try us.posthog.com, then eu.posthog.com"
region-discovery loop, and they drifted. The driver skips a region
that rejects the token (wrong region) and uses the one that answers
2xx; the probe instead returned "credential rejected" on the first
region's 401/403, before ever trying the second.
PostHog Cloud US and EU are separate deployments, so an EU OAuth
token is a 401 on us.posthog.com, which is probed first. The probe
bailed there and marked the source disconnected, while access-review
campaigns -- which use the driver -- kept working.
Delete the probe's copy and delegate to the driver's now-exported
ResolvePostHogRegion, the single resolver the campaign also uses. It
flags a credential every region rejected (ErrPostHogCredentialRejected)
apart from a transient failure on the token's own region, so the probe
marks a source disconnected only for a genuinely dead token and does
not flap on a passing 5xx.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Note that Cloudflare requires per_page in 5..50 so nobody optimizes
it back to per_page=1, which returned 400 and, before terminal
classification, produced a 400 storm. Also correct a stale team_id
reference in the Vercel user-fetch comment: the OAuth callback
surfaces the team as teamId.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The Brex name resolver calls GET /v2/company to build the source
display name, but the connector only requested openid, offline_access,
and users.readonly -- and users.readonly covers /v2/users (the account
fetch), not /v2/company. So the company endpoint 403'd for every Brex
source, which the source-name worker retried forever (2.9M errors in 7
days) until the terminal-error handling stopped the loop.
Add companies.readonly so the endpoint resolves. Verified as the exact
scope string against Brex's OAuth authorize flow (the "Companies: Read
only" developer scope maps to companies.readonly). Existing Brex
connectors must reconnect to re-consent to the added scope; until they
do, /v2/company still 403s but is now handled cleanly (terminal ->
generic name, no loop) rather than silently retried.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Clerk's Backend API secret key only exposes the customer's application
end-users (GET /v1/users) -- the people who sign into the customer's
product -- not the Clerk workspace/dashboard team who administer the
platform. Those admins have no Backend API and are managed only in the
Clerk Dashboard UI. So an access-review campaign against Clerk reviews
the wrong population entirely (verified in prod: one source paginating
16K+ end-users), which is a compliance defect, not just the pagination
timeouts it also caused.
Unregister Clerk from the builtin provider registry and drop its driver,
provider registration, tests, and cassette so it can no longer be added
or fetched. Keep the ConnectorProviderClerk enum value, its IsValid
membership, the GraphQL enum value, and the frontend logo so existing
stored CLERK connectors still validate, serialize, and display (users
can still see and remove them). Drop it from ConnectorProviders so the
registry stays in sync with the registered set.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:
- Convert every source-file header to the MIT text across all comment
styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
"MIT License" title line
- Switch the package.json license fields, Docker image label, and
cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
(Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
the comma-separated years to a hyphenated range
Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
RequiresManagedResourceID only has meaning for a ManagedAPIKey provider:
ManagedConnectorReady consults it exclusively on that path. A non-managed
provider that set it would advertise normally with the requirement
silently doing nothing. Reject the combination at registration, matching
the ManagedAPIKey/SupportsAPIKey mutual-exclusion guard already in
Register, and cover it with a TestRegistry_Register subtest.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The Crisp and Railway connectors each repeated their HTTP plumbing
across the driver, the name resolver, and (for Crisp) the subscription
settings fetcher: the same JoinPath/headers/Do for Crisp GETs and the
same marshal/POST/headers for Railway GraphQL. The Crisp and Scaleway
drivers also carried byte-identical owner/member role mapping and admin
checks.
Extract crispGet and railwayPost as package-private request helpers so
each call site owns only status handling, and lift the owner/member role
mapping into shared ownerMemberRoles/isOwnerRole helpers beside
activeFromStatus in driver.go. Name the Crisp base URL and tier header
as consts in probe.go's const block rather than inlining the literals,
matching the file's existing convention. Behavior is unchanged.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A managed (Model B) connector like Crisp needs both the Probo-held key
and a resource ID (the plugin ID) to connect, but the driver catalog
gated visibility on the key alone. A deployment that set the key without
the plugin ID (reachable through raw JSON config; the bootstrap env path
already requires both) would show Crisp as connectable and then fail
every attempt with an internal error.
Add a RequiresManagedResourceID flag to the registration and a
Registry.ManagedConnectorReady check that requires both before a managed
provider enters the catalog, so a half-configured provider stays hidden
instead of dead-ending at connect.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Crisp is a managed (Model B) connector: Probo holds one plugin token
server-side and each connection carries only a Website ID. Nothing
stops one organization from entering another organization's Website
ID, so prove control of the website before creating the connection.
Probo derives a per-(organization, website) verification code as an
HMAC over the token secret and exposes it through a new
crispVerificationCode query. The customer pastes it into the Probo
plugin's per-website settings; at connect time the resolver reads the
setting back through the managed plugin token and requires a
constant-time match before any row is written. The managed key and
plugin ID come from bootstrap, so the connector stays hidden until the
deployment configures them.
The settings fetch is injected so the create-time gate's branch wiring
is unit-tested (mismatch and not-subscribed reject, internal errors
stay generic, a matching code passes), and the managed-versus-client
key resolution is covered too.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Four API-key, single-tenant (Pattern 3) connectors:
- Scaleway: secret key in the X-Auth-Token header plus an Organization ID
setting; GET /iam/v1alpha1/users (owner/member, status, two-factor),
per-connection BuildProbeURL.
- Yousign: Bearer API key; GET /v3/users (admin/owner/member, is_active);
production host with a static probe.
- Railway: Bearer account token; GraphQL me{workspaces{members}} aggregated
and deduplicated across workspaces; custom probe, since Railway returns
HTTP 200 with an errors body on a rejected token.
- Crisp: plugin token as HTTP Basic (identifier:key) plus a Website ID
setting and the X-Crisp-Tier header; GET /v1/website/{id}/operators/list,
custom probe and name resolver.
Scaleway and Crisp carry a required extra setting, so the console add-source
dialog maps organizationId/websiteId onto their scalewayOrganizationId and
crispWebsiteId API-key inputs; without that mapping the value is silently
dropped and the create is rejected.
Cassette-backed driver tests plus unit tests for the cross-workspace
deduplication, the probe contracts and the role/MFA helpers.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add Pylon, OpenRouter, incident.io and Brevo as access-review connectors.
All are API-key, single-tenant providers (Pattern 3): the key identifies
one tenant, so there is no OAuth flow, picker UI, or bootstrap/helm
configuration.
- Pylon: Bearer token, GET /users; resolves each user's opaque role_id to
a role name via GET /user-roles, with cursor pagination.
- OpenRouter: Bearer management key, GET /api/v1/organization/members. The
endpoint requires an organization account -- a personal key authenticates
but returns 404 -- so the connection probe rejects 404 on top of 401/403
(doProbeRequest gained an opt-in extra-reject set) to surface a non-org
key at connect time instead of mid-campaign.
- incident.io: Bearer token, GET /v2/users. Its OAuth is outbound-only, so
the API key is the inbound path; live base_role/custom_roles take
precedence over the deprecated role enum.
- Brevo: API key in the api-key header (Registration.APIKeyHeader), GET
/v3/organization/invited/users. A live recording corrected the documented
schema: is_owner is a JSON boolean (not a string) and an id field is
present, so it is used as the stable ExternalID.
The OpenRouter and Brevo cassettes are anonymized live recordings; Pylon
and incident.io use hand-authored fixtures (no self-serve test tenant). The
shared three-valued active-status mapping is consolidated into
activeFromStatus in driver.go.
Each adds the enum value, migration, GraphQL binding, provider
Registration, a driver with a cassette-driven test, and a brand logo.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
DocuSign approved our partner integration, so the connector can now
complete a real OAuth2 authorization-code flow. The integration key
has PKCE enabled, so RequiresPKCE is set; the confidential grant still
authenticates the token exchange with Basic auth and replays the
verifier as the documented hardening layer.
A DocuSign user may have access to several accounts, so this replaces
the previous auto-default-account behavior with a Pattern-1 picker:
the user chooses the account after OAuth, the choice is stored on
DocuSignConnectorSettings, and the driver and name resolver resolve
the selected account's data-center base URI from /oauth/userinfo.
Other changes:
- Request the extended scope so the refresh token's 30-day window
rolls on each use; without it the token hard-expires 30 days after
consent and breaks the connection.
- Drop API-key support: DocuSign has no static API key, only OAuth.
- Return ("", nil) from the name resolver on terminal failures so the
source-name worker does not retry a revoked token forever.
- Add a driver test and cassette; the test previously skipped in CI
for lack of a cassette.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Heroku's connection-status probe used a static ProbeURL, which the
generic probe issues with `Accept: application/json`. Heroku negotiates
the API version through the Accept media type and returns 400 for an
unversioned request, which doProbeRequest reads as "connected" -- so the
probe never caught a revoked token (it only surfaced at the first
ListAccounts).
Probe via a probeHeroku closure that sends
`Accept: application/vnd.heroku+json; version=3` instead. Verified live:
a dead token returns 400 with application/json but 401 with the
versioned header, which doProbeRequest correctly maps to rejected.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add Mercury, Apollo.io, Deepgram, ClickHouse Cloud, and Langfuse as
access-review connectors. All are API-key, single-tenant providers
(Pattern 3): the key identifies one tenant, so there is no OAuth flow,
picker UI, or bootstrap/helm configuration.
- Mercury: Bearer token, GET /api/v1/users, cursor pagination.
- Apollo.io: x-api-key header, GET /api/v1/users/search (teammates).
- Deepgram: Token scheme; lists members across every project and
dedupes by member_id, unioning per-project scopes.
- ClickHouse Cloud: HTTP Basic (keyId:keySecret); discovers the org
via GET /v1/organizations, then lists its members.
- Langfuse: HTTP Basic (publicKey:secretKey); a base-URL setting
selects the regional cloud host or a self-hosted instance.
Each adds the enum value, migration, GraphQL binding, provider
Registration, a driver with a cassette-driven test, and a brand logo.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The API-key connection transport could present a key as a Bearer token,
an x-api-key header, a custom scheme (SSWS/Token), or HTTP Basic with an
empty password (Cursor). None of these can carry a real password, which
providers such as ClickHouse Cloud (keyId:keySecret) and Langfuse
(publicKey:secretKey) require.
Add a fourth mode, APIKeyBasicAuthUserPass, that base64-encodes the
stored "username:password" credential verbatim into Authorization: Basic.
SetBasicAuth cannot express this -- it re-appends a ":" and corrupts the
credential. The mode is wired generically through the registry and the
create-connector resolver and is mutually exclusive with the other
API-key auth modes.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Bad API keys and expired OAuth tokens showed Connected because
probes ran only for OAuth2 and many providers had no ProbeURL.
Add a registry ProbeConnection dispatcher with static, dynamic,
and custom probes so all 41 providers are checked on demand.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Replace the bare `getprobo.com` domain and the `www.getprobo.com`
marketing host with `probo.com` / `www.probo.com` across the codebase.
Functional subdomains (app, console, notification, custom, test,
cookie-banner, compliance) keep their existing `getprobo.com` hosts,
and changelog entries are left untouched.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Register Neon as a connector provider and add a new access review
driver that fetches organization members from the Neon API with
cursor-based pagination.
Neon's OAuth is partner-gated, so the connector is API-key only
(Bearer, the default scheme). A personal or organization API key can
belong to several organizations; the operator supplies the ID of the
one to review. The members endpoint exposes per-user MFA state
(has_mfa) and deactivation, which map to the access entry MFA status
and active flag; the stable account UUID (user_id) is used as the
external ID over the membership ID.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Register Render as an API-key connector provider and add an access
review driver that fetches workspace members from the Render API
(GET /v1/owners/{ownerId}/members).
Render exposes no partner OAuth program, so the connector authenticates
with a read-scoped API key (Authorization: Bearer) plus the customer's
Workspace ID. The flat members endpoint reports an explicit account
status and MFA flag, surfaced as the Active and MFAStatus fields; the
stable "usr-" id becomes ExternalID. There is no picker -- the
workspace is captured up front via ExtraSettings -- so
SetOrganizationSettings is omitted.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Register Qovery as a connector provider and add a new access review
driver that fetches organization members from the Qovery API.
Extend API key connection handling with a configurable Authorization
token scheme so Qovery can use "Token" while existing providers
continue to default to Bearer.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Better Stack exposes team members and pending invitations through its
Uptime API. Wire it as an access-review connector so a Better Stack
team can be reviewed in access-review campaigns.
Better Stack has no third-party OAuth app for listing members (its
OAuth is an end-user MCP sign-in), so the connector authenticates with
a Bearer API token plus the team name that scopes the team-members
listing. The driver paginates /api/v2/team-members, maps roles and
invitation records into account records, and the source name is
resolved from the configured team.
This wires the full surface: the provider enum and migration, the
connector settings, the registry registration with the team-name extra
setting, the GraphQL input and resolver marshaling, the frontend field
mapping and connector logo, and cassette-backed driver tests.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add a SigNoz connector so its organization members can be pulled into
access-review campaign snapshots. SigNoz authenticates with a
SIGNOZ-API-KEY admin service-account key and a customer-supplied base
URL (a SigNoz Cloud region/tenant host or a self-hosted instance).
The driver lists users via GET /api/v1/user, which returns the role
(ADMIN/EDITOR/VIEWER) inline so admin detection works in a single call,
and maps the SigNoz user status (active / pending_invite / deleted) to
the account active flag. The name resolver reads the organization
display name from GET /api/v2/orgs/me to title the access source.
Wire the provider through the coredata enum and settings, the
connector-provider registry (driver and name-resolver factories), the
console API-key input schema and validation, the access-review source
label, and the SigNoz brand logo.
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
BuildTokenURLForDomain and BuildTokenURLForSite cannot be merged: a
provider's signed-state Site (e.g. Datadog's region key) is not always the
string its token host needs (the API domain). Spell that out at the switch
so the split is not mistaken for accidental duplication.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A Registration that sets both BuildTokenURLForDomain and
BuildTokenURLForSite would pass startup but silently use only the first
at the OAuth callback (CompleteWithState checks them in order). Reject
the combination in Register so the misconfiguration fails loud at
process start, matching the existing APIKeyBasicAuth/APIKeyHeader guard.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Zendesk is a multi-tenant OAuth connector keyed by the customer
subdomain. The customer enters it at connect time; it rides the signed
state to the callback, is re-validated, and is stored on the connector
settings to build the API host.
List staff (agents and admins) via GET /api/v2/users.json with cursor
pagination, mapping role, active/suspended, and 2FA status; end-users
are excluded. The subdomain is validated as a single DNS label at every
trust boundary to close the SSRF vector, and the data client keeps the
SSRF-protected transport.
Zendesk OAuth across customer subdomains requires a Zendesk-approved
global OAuth client; the connector goes live once those credentials are
supplied via bootstrap.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Subdomain-based OAuth providers (e.g. Zendesk) authenticate against a
per-customer host, <subdomain>.zendesk.com, for both the authorize and
the token endpoint. The subdomain is known at initiate but, unlike
Datadog's region, the provider does not echo it back on the callback,
so it must survive the round-trip another way.
Carry the chosen site on the HMAC-signed OAuth state (OAuth2State.Site)
and add a BuildTokenURLForSite closure, symmetric with the existing
BuildAuthURLForSite, that builds the token URL from it at callback time.
The state is signed, so a tampered site is rejected before use; the
closure still re-validates the format. Datadog's domain-echo path is
unchanged and a provider sets at most one of the two closures.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
OktaProvider was dead: the provider-string const is only matched by
OAuth providers (SlackProvider in oauth2.go / connector.go), and an
API-key connector never matches on it.
Wrap the url.Parse error in nextPageURL with %w, like every sibling
driver, instead of dropping it.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Register the Okta provider (SupportsAPIKey, APIKeyAuthScheme SSWS, a
required "domain" extra setting, and the driver/name-resolver
factories) and add it to the builtin registry.
The create-API-key resolver normalizes and validates oktaDomain into
OktaConnectorSettings, returning a static INVALID error that never
echoes operator input, and stamps the SSWS scheme onto the
connection. No picker, OAuth metadata, or probe URL: the token plus
domain identify exactly one org and the host is per-connection.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The driver lists GET /api/v1/users (limit=200) on the customer's org
host and follows the RFC 5988 Link header, pinning pagination to the
configured host so a response cannot redirect the crawl off-tenant.
User status maps to the three-valued Active flag (SUSPENDED and
DEPROVISIONED are inactive); ExternalID is the stable Okta user id.
The name resolver reads /api/v1/org and returns ("", nil) on any
non-2xx so a read-only token lacking org-settings read does not loop
the source-name worker.
The org domain is operator-supplied and feeds the URL host, so it is
the one SSRF-sensitive input: NormalizeOktaDomain validates and
strips it on the write path and IsValidOktaDomain re-checks it at
driver construction, on top of the transport's SSRF protection.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Okta API tokens authenticate as "Authorization: SSWS <token>", a
scheme none of the existing API-key modes (Bearer, custom header,
Basic) can express. Add Registration.APIKeyAuthScheme, plumb it onto
APIKeyConnection.Scheme, and send it via a new schemeAuthTransport.
The three API-key presentations (BasicAuth, Header, Scheme) are
mutually exclusive; Register rejects setting more than one so a
misconfiguration fails at process start rather than silently.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
GET /v1/users returns a bare JSON array, not a {"data":[...]} envelope
(total_count is a separate endpoint), so decode directly into a slice.
The previous envelope-first decode errored on the array and never
reached the fallback, so ListAccounts failed against the real API.
Treat deprovisioned users as inactive. Add the missing Clerk
third-party logo and its wiring, and document why the registration
sets no probe URL or name resolver.
Record the driver-test cassette against a live Clerk development
instance, scrubbed of PII and instance identifiers (emails to
example.com, image_url payloads and CF transport headers dropped).
A locked account verifies the inactive path.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Wire Clerk in as a supported connector provider for access\nreviews and expose it through the console GraphQL provider enum.\n\nAdd a dedicated Clerk driver that lists users from the Clerk\nBackend API, maps account state and authentication signals into\nAccountRecord fields, and covers the behavior with focused driver\nand provider tests.\n\nInclude a migration that appends CLERK to the connector_provider\nenum so environments can persist Clerk connectors safely.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
The teammate detail endpoint returns a bare object, not a {"result":...}
envelope, so the N+1 scope fetch silently discarded scopes and MFA was
always Unknown for every teammate. Decode the bare object.
Found by re-recording the driver cassette against the live API; the
hand-written cassette had hidden the mismatch. Also from that review:
- Map is_sso/is_partner_sso to AuthMethod (SSO vs Password) instead of
hardcoding Unknown
- Log the swallowed teammate-detail fetch error (wire the driver logger,
matching github.go)
- Add a restricted teammate + a missing-email row to the cassette so the
N+1 decode, the empty-email skip, and the SSO path are load-bearing
- Drop the stale Content-Length response header; document ExternalID
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
- Add SendGrid third-party logo and wire it into ThirdPartyLogo
- Add SendGrid name resolver (account company name, graceful fallback)
- Fix MFA detection: full-access teammates carry both 2fa_exempt and
2fa_required, so report Unknown unless exactly one is present
- Re-record the driver cassette against the live API
- Use a random time suffix for the migration filename
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Implement a SendGrid access-review driver that fetches teammates
from the SendGrid API and maps them into AccountRecord values.
Register SendGrid as a connector provider, expose it through the
connector provider enum, and add a migration that extends the
connector_provider type with SENDGRID.
Cover the new driver with a VCR-backed fixture test and helper
tests for role and response-shape handling to keep parsing robust.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>