Remove schema objects that are no longer referenced by any query code,
GraphQL resolver, or frontend and only survive in old migration files:
- users_organizations: superseded by iam_memberships (data was migrated in
20251006T220024Z).
- organizations.logo_object_key: pre-files-table logo storage; replaced by
logo_file_id / horizontal_logo_file_id (data migrated in 20251009T140000Z).
- trust_center_accesses.has_accepted_non_disclosure_agreement,
has_accepted_non_disclosure_agreement_metadata, nda_file_id and
last_token_expires_at: the NDA-acceptance flow now uses
electronic_signature_id together with the state column.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Instead of a toast, expired magic link tokens now return a TOKEN_EXPIRED
GraphQL error code and redirect users to a dedicated /magic-link-expired
page with a clear CTA to request a new link.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@probo.com>
Using localhost as the EHLO identity is the Go net/smtp default but is
rejected by strict relays such as Google Workspace. os.Hostname() is
the conventional SMTP client fallback and returns the actual machine or
pod name in production environments.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Google Workspace SMTP relay (smtp-relay.gmail.com) rejects generic
EHLO identifiers such as "localhost", which is Go net/smtp's default.
Operators can now set SMTP_HELLO_NAME to a valid hostname so that
c.Hello() is called before StartTLS, satisfying strict relay policies.
Fixes https://github.com/getprobo/probo/issues/1284
Signed-off-by: Bryan Frimin <bryan@probo.com>
Rejecting one audit report via Slack could look like a blanket denial
when HIPAA and SOC 2 reports shared a filename. Use framework and
audit name in rejection emails.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.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>
Drop the zendeskRole helper and use the raw API role, matching the other
drivers. Factor the duplicated RFC3339 timestamp parsing (parseZendeskTime
and parseDatadogTime were identical) into a shared parseRFC3339Ptr in
driver.go.
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>
The rel="next" link is taken from the provider response, so the host
check alone let a response downgrade the next request to http or move
it to another port on the same host. Reject a non-https scheme and any
explicit port in addition to a host mismatch, so the crawl stays on
the same https origin.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The driver test cassette was regenerated by running the driver in
record mode against a live Okta org, which validated the field
mapping (id, status, created/activated/lastLogin, profile.*) and the
rel="self" Link header format against the real GET /api/v1/users
response.
The recording is then fully anonymized: the real domain, member
names, emails, user IDs, session cookie, and request-id are replaced
with synthetic values, and a second page with a rel="next" Link
header is added back to keep pagination coverage (the source org had
a single user). TestCassettesUseSyntheticEmails guards the result.
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>
Introduce the OKTA value to the connector_provider enum (Go const,
ConnectorProviders slice, IsValid switch, GraphQL @goEnum, and the
Postgres ALTER TYPE migration) and an OktaConnectorSettings struct
holding the customer's Okta org domain.
Okta is a per-tenant IdP with no central API gateway, so the
connector is keyed on the org domain rather than a shared host. The
oktaDomain field on CreateAPIKeyConnectorInput lets the API-key flow
carry it.
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>
Align the Clerk driver tests with the rest of the access-review\ndriver suite by exercising VCR cassette replay instead of a\ncustom transport stub.\n\nThis keeps test coverage consistent with existing providers and\nensures request matching behavior is validated against a checked-in\nfixture.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
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>
Add a best-effort MFA status check to the SendGrid access-review
fetch pipeline by querying teammate details and inspecting 2FA scopes.
When teammate scopes include 2fa_required or 2fa_exempt, map those
to ENABLED or DISABLED MFA status values; otherwise keep UNKNOWN.
Extend the SendGrid cassette and tests to cover the detail lookups and
MFA scope mapping behavior.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
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>
Fold POSTHOG_SELF_HOSTED into POSTHOG: one provider now covers Cloud (OAuth + region-pinned API key) and self-hosted (API key + instance URL), since both already share the driver, name resolver, and PostHogConnectorSettings{BaseURL}. The API-key form picks a deployment (Cloud US/EU or self-hosted URL); the resolver requires exactly one of region/instanceUrl.
Drop the POSTHOG_SELF_HOSTED enum value, registration, migration, and logo mapping. Extract the deployment selector into a dedicated PostHogDeploymentField component. Point the driver tests at us.posthog.com instead of the legacy app.posthog.com host.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The us/eu host strings lived in the driver and were duplicated in the
connector-settings resolver. Expose drivers.PostHogRegionBaseURL as the
single source and call it from the resolver.
Also parse the base URL only inside the absolute-next branch of
resolveNextURL, where it is actually used for the host check.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
buildTokenRequest repeated the form-encoded POST construction and the
shared headers across the basic-form, none and post-form branches. Extract
newFormTokenRequest, and fold the public-client none case into the default
branch — it differs only in omitting client_secret.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Marshal the region (us/eu) for PostHog Cloud and the instance URL
for PostHog Self-Hosted from the create-connector input, validating
each, and add the matching GraphQL inputs and the POSTHOG_SELF_HOSTED
enum value.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
PostHog Cloud authenticates via CIMD OAuth (public client, PKCE)
through the region-agnostic oauth.posthog.com gateway, with an API-key
fallback. PostHog Self-Hosted is a separate provider using an API key
and an instance URL.
The shared driver discovers the data region by probing us/eu for OAuth
connections, since the gateway does not serve the data API, and pins
pagination to the resolved host.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Public-client (CIMD) providers need no operator credentials: derive
their client_id from the deployment base URL and their state-signing key
from the active OAuth2 server signing key, registering them at startup
unless the operator configured them explicitly.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A public client identifies itself to the provider with a hosted
Client ID Metadata Document. Serve it unauthenticated and outside the
auth group, since the provider fetches it server-to-server, exposing the
deployment-derived client_id and redirect_uri alongside the Probo brand
name, homepage and logo.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Public clients authenticate with PKCE and no client secret, using a
hosted Client ID Metadata Document (CIMD) as the client_id.
Add a no-secret token-endpoint mode, derive the state-token salt and the
PKCE verifier from a server-side key so the verifier never appears in
the signed-but-unencrypted state, and expose Registration.PublicClient,
Registry.PublicClients and the CIMD metadata path for provider wiring.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
CommonThirdParty.logoUrl and TrustCenterReference.logoUrl were
returning expiring S3 presigned URLs, which break if cached or
shared past their TTL.
Replace with stable /api/files/v1/{id} application URLs.
file.Service now generates these via baseurl; a new filesign
package owns presigning for the files/v1 HTTP handler that
does the internal redirect.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>