Cut verbose inline comments across the access-review connector changes:
the mechanical fact stays at the read site, while incident backstory
("millions of error logs in prod") and provider-specific rationale (why
Clerk reviews the wrong population) move to the commit history where they
belong. Also tighten a loose "a 4xx" to "an auth/not-found 4xx" so the
terminal-classification contract is not overstated. No behavior change.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The Vercel connector reads the team from the camelCase teamId callback
parameter, unlike the snake_case params most providers use. Nothing
guarded the name, so a regression back to team_id would silently drop
the team on every Vercel connect and leave the source resolving no
users.
Extract the read into vercelCallbackTeamID and pin the exact parameter
name with a test, since the surrounding handler needs a live connector
registry and database to exercise directly.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Once the source-name worker sets name_synced_at, nothing cleared it,
so a source that hit a terminal failure (Brex 403, wrong Sentry org
slug) kept its generic name forever — even after the user reconnected
with the right scope or picked the correct org, contradicting the
Brex guidance to reconnect.
Clear name_synced_at whenever the connection changes: on UpdateSource
when a connector is (re)set, inside ConfigureAccessReviewSource when
the org is (re)selected, and on the OAuth reconnect path via the new
ResetSourceNameSyncForConnector service method. The worker then
re-claims the row and re-resolves the display name.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Vercel appends the customer's team to the OAuth completion callback as
teamId (camelCase), but the handler read query.Get("team_id"). The
lookup always missed, so every team install fell through to the
/v2/user personal-account fallback -- which returns 404 for a
team-scoped integration token -- leaving TeamID empty. The Vercel
driver then refused to build ("team_id is required") and every
access-review campaign targeting a Vercel team fetched zero accounts.
Personal-account installs were unaffected because they never send a
teamId and legitimately use the /v2/user path, which is why the
breakage only showed up for team installs.
Read the parameter under its real name so the team is captured and
persisted in the connector settings.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Replace flattened SSL fields with a certificate
relation loaded through certmanager, and resolve
domain slots from IDs already on the trust center
instead of reloading the compliance page.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Remove the root complianceportal package and the resolver
facade that existed only to break an IAM import cycle. Admin
policies, domain URL helpers, and actions live under
management; visitor OAuth metadata, brand URLs, and public
read paths live under visitor. Drop the duplicate trust API
magic-link mutations now that Connect handles portal auth, and
stop IAM from owning compliance page email branding.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Rewire the console and visitor resolvers onto the management and visitor
services with compliance-portal authorization. Rename the GraphQL and MCP
ComplianceExternalURL type to ComplianceCustomLink, expose trust center
profile fields, default and custom domains, public URL, and the managed
flag, and drop the profile fields from the organization surface.
Signed-off-by: Bryan Frimin <bryan@probo.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>
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>
The GraphQL endpoint built its gqlgen server with bare handler.New and
no limits, so a single request with thousands of aliased resolver calls
was parsed, validated, executed, and marshalled in full. Under load this
let an unauthenticated client drive excessive CPU and memory use against
POST /api/connect/v1/graphql and the console and trust endpoints, which
share the same constructor (GHSA-prh2-g8pv-m7p9).
Add configurable guards in the shared gqlutils.NewHandler: a parser
token limit rejects oversized queries at lex time before any execution,
a fixed complexity limit caps field-selection count, an LRU query cache
avoids repeated parsing, and field suggestions are disabled. The limits
flow from a new APIConfig.GraphQL section through server and api config
into all three GraphQL handlers, with PROBOD_API_GRAPHQL_* env vars and
Helm values exposed for per-environment tuning.
Defaults are sized with generous headroom over real traffic: the parser
token limit (15000) and complexity limit (2000) sit far above the
largest legitimate frontend query yet well below the proof-of-concept
flood, so normal usage is unaffected while floods are rejected cheaply.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Introduce BearerChallengeMiddleware on MCP, Console and Connect GraphQL, Files, and OAuth2 userinfo. Call sites record challenge intent in context via NoteUnauthenticated, NoteInvalidToken, and NoteInsufficientScope; the middleware applies resource_metadata, invalid_token, and insufficient_scope on WriteHeader.
OAuth2 access token middleware flags rejected Bearer tokens for invalid_token challenges. Add Authorizer.ScopesForAction for the scope auth-param.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Drop the setTrustCenterAlias and removeTrustCenterAlias mutations and
the alias field on Audit in favor of generic setResourceAlias and
removeResourceAlias mutations backed by the resourcealias service.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Register v1 API scopes in coredata, advertise them in OIDC discovery
and protected-resource metadata, show them on the consent screen, and
enforce scope-to-action mapping in the IAM Authorizer before policy
evaluation.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Trust GraphQL and MCP still exposed presigned URL strings for
trust-center logos while console and connect already serve stable
File.downloadUrl paths. Phase 1 migrates the seven public logo
fields on trust GraphQL and the trust-center file references on MCP
to the shared File type; trust GraphQL NDA stays on fileUrl for a
follow-up.
Trust resolvers load public files through filemanager and map them
with types.NewFile. The trust app Relay queries and components now
read logo.downloadUrl. MCP specification, resolvers, and helpers
are updated in sync, including NDA on MCP where callers already
have file access.
filemanager is split into focused files and its URL surface is
narrowed to GenerateFileURL(file) for stable app URLs and
GeneratePresignedURL for S3 redirects. GetPublicFile remains the
DB entry point when only a file ID is known.
Add trust and MCP e2e coverage for public logo download URLs.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Move agent-run orchestration from the legacy supervisor path into the new
agentrun worker/service package and wire it through coredata, server,
policies, and GraphQL resolvers.
This consolidates run lifecycle handling around lease-aware workers and
aligns API surface with the new agent-run domain model so reviewers can
follow one coherent execution path.
Signed-off-by: Bryan Frimin <bryan@probo.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>
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>
Each tracker pattern carries either a direct org-scoped third_party_id
or an indirect link via common_tracker_pattern_id, but the console API
never surfaced either. Expose two optional resolver-driven fields on
the GraphQL TrackerPattern node:
thirdParty: ThirdParty
commonThirdParty: CommonThirdParty
The org-scoped ThirdParty takes priority. When ThirdPartyID is set the
commonThirdParty resolver short-circuits to nil, so the chained
common_tracker_pattern -> common_third_party lookup is only paid for
when a pattern has not been promoted to a tenant-managed third party.
To make the resolver pattern viable across paginated banner trackers
listings, the model now uses @goModel and a custom struct that carries
the foreign-key handles (ThirdPartyID, CommonTrackerPatternID) without
exposing them in the schema. NewTrackerPatternNode populates them from
coredata.
Two new request-scoped dataloaders (CommonTrackerPattern,
CommonThirdParty) batch the chained lookup, mirroring the existing
ThirdParty / CookieCategory loaders. The console mux now wires the
third-party service through dataloader.NewMiddleware so the second
loader has its backing service.
Authorization follows existing precedent: ActionThirdPartyGet for the
org-scoped lookup, ActionCommonThirdPartyGet (granted by the
identity-scoped CommonThirdPartyCatalogPolicy) for the catalog lookup.
ErrResourceNotFound and dataloadgen.ErrNotFound are mapped to a null
field rather than an error.
Signed-off-by: Émile Ré <emile@probo.com>
The console previously dispatched per-provider logic through a fan of
init()-side-effect maps (driver names, OAuth2 metadata, probe URLs,
display names, settings switches), spread across pkg/connector,
pkg/accessreview/drivers and the console v1 resolvers. Adding a new
provider required edits in every one of those places and a corresponding
switch arm in CreateConnectorRequest. The same per-provider knowledge
also leaked into Helm templates as hand-rolled environment-variable
blocks per connector.
This commit collapses the dispatch surface into a single typed
*provider.Registry. The registry is constructed once by
NewBuiltinRegistry at probod startup and threaded as an explicit
dependency into every consumer (accessreview service, console v1
resolver, OAuth2 wiring). There is no package-level state. Each
provider lives in one file under pkg/connector/provider/ that exposes
a private xxxRegistration() *Registration constructor; NewBuiltinRegistry
enumerates them.
CreateConnectorRequest loses its per-provider settings fields and
takes a single RawSettings json.RawMessage produced by the
per-provider MarshalSettings closure. The 1Password SCIM bridge URL
is validated at create time (http(s) scheme + non-empty host) so a
malformed value fails fast at the resolver boundary. The Helm chart
gains probo.connectorEnv and probo.connectorSecretEntries templates
so adding a connector requires zero Helm changes. Access-review name
resolution moves into the same Registration value to keep one
authoritative dispatch table.
Tests cover every Registration (DisplayName, NewDriver wired),
Register error paths (nil, empty Provider, empty DisplayName,
duplicate), All / ProviderDisplayName / ProviderOAuth2Scopes /
ProbeURL hit and miss paths, the ApplyOAuth2Defaults templating and
PKCE branches, and ConnectorSettings[T] round-trip plus malformed-JSON
error path. The pre-refactor ApplyProviderDefaults test in
pkg/connector is replaced by the equivalent in
pkg/connector/provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Resolving a typical Console GraphQL query triggers many parallel
authorize calls (one per resource per field resolver). This commit
collapses them via a dataloader: parallel calls within the same
request are gathered into a single iam.Authorizer.AuthorizeMulti pass,
and only fall back to per-item Authorize when AuthorizeMulti rejects
the whole batch (e.g. mixed organizations).
The loader key encodes resource id, action, options, and a canonical
JSON-encoded attribute map so logically identical calls share a key
while differing ones do not. The loader is created without caching so
repeated calls within a request still produce one audit log entry per
call. dataloader.NewAuthorizeFunc preserves the existing
authz.AuthorizeFunc signature and error mapping.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Introduce a hierarchical risk assessment model with six entity types:
- Risk Assessment: top-level container scoped to an organization
- Risk Assessment Scope: sub-container for scoping threat modeling
exercises within an assessment
- Risk Assessment Node: DFD elements typed as ENTITY, BOUNDARY,
ASSET, or DATA within a scope
- Risk Assessment Process: directed data flows between two nodes
- Risk Assessment Threat: descriptive threats attached to a process
with a free-text category (e.g. Confidentiality, Integrity)
- Risk Scenario: thin join linking a threat to a risk from the
register, carrying only a name and description
Risk scoring (likelihood, impact, treatment) remains on the existing
Risk entity. Threats are purely descriptive. Risk Scenarios connect
the threat model to the risk register without duplicating scores.
Backend: migration with PG enum for node types, coredata structs,
service layer with full CRUD and validation, GraphQL schema with
18 mutations and paginated connections, authorization actions and
policies, and base_resolvers.go Node dispatch for all entity types.
Frontend: Risk Assessments list page with create dialog, detail page
showing scopes as cards with nodes/processes/threats tables, inline
create/edit/delete actions on all entities, and a Scenarios tab on
the Risk detail page linking threats to risks. Existing RiskGraph.ts
hook file removed in favor of colocated queries in page files.
E2E tests cover CRUD for all entity types, RBAC, and tenant
isolation.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
- Rename access-review migration with random time
- Move PagerDuty token-response handling to its own file
- Strip OAuth error_description from log and redirect
- Validate PagerDuty subdomain on OAuth callback
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
- Wire post-OAuth pickers and 2-auto callback handling
- Add 13 vendor logo components for new connectors
- Wire access-review connectors into bootstrap config
- Track e2e gap for new access-review connectors
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The CreateVendorDialog previously loaded the entire @probo/vendors
JSON bundle client-side and used MiniSearch for fuzzy search. This
replaces it with a GraphQL query against the common_third_parties
database table, searched server-side via ILIKE filtering.
Backend: adds CommonThirdParty GraphQL type, a pkg/thirdparty
service, and a commonThirdParties(name) root query. Frontend:
splits into CommonThirdPartyCombobox (display) and an @inline
fragment read on selection via readInlineData.
Signed-off-by: Émile Ré <emile@getprobo.com>
Batch-load CookieBanner and CookieCategory entities via
dataloadgen instead of making individual service calls in
GraphQL resolvers, matching the existing dataloader pattern
used for organizations, frameworks, etc.
Signed-off-by: Émile Ré <emile@getprobo.com>
Parallel gqlgen instances race on the Go build cache, causing
Rewriter.getSource() to panic with "slice bounds out of range"
when go/packages returns empty source. Chain gqlgen targets with
order-only prerequisites in the Makefile and switch the CI build
job to sequential generation.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Implement a full OAuth2 2.0 and OpenID Connect 1.0 authorization
server with support for authorization code flow (with PKCE),
refresh token rotation, device authorization grant, dynamic
client registration, token introspection, and token revocation.
Includes database schema, coredata layer, service logic, HTTP
handlers, OIDC discovery endpoint, and JWKS publishing.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
The connector initiate and complete HTTP handlers used panic for
operational errors (network, DB, provider failures). No recovery
middleware exists on the console chi router, so these panics
produced incomplete responses instead of proper HTTP 500 errors.
Use the same log-and-render pattern already established in
loadExistingConnector error handling.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The initiate handler now looks up the existing connector for the
target (organization, provider) pair, reads its stored scope set
through Connection.Scopes, and unions it with the scopes the caller
passed in the query string. The union is what gets requested on the
OAuth authorization URL, so reconnects never drop a previously
granted scope.
When an existing connector is found the handler also flags the
flow as a reconnect via InitiateOptions.ConnectorID, so the
OAuth2 state carries the id and the callback updates the row in
place. When the provider supports it (Google Workspace), the auth
URL also carries include_granted_scopes=true and the user sees
only the delta on the consent screen.
There is no short-circuit: every initiate click runs the full
OAuth flow even if stored scopes already cover the request, because
scope coverage is an unsafe proxy for token liveness. Revoked
tokens or leftover connectors from deleted access sources would
otherwise be silently reused.
The handler body is extracted to its own file to keep NewMux
readable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Reconnect now takes a ReconnectConnectorRequest carrying the expected
OrganizationID and Provider. It validates inside the same transaction
that the loaded connector belongs to the requested org, provider and
OAUTH2 protocol before mutating the row. This blocks cross-org and
cross-provider corruption via a crafted connector_id reaching the
OAuth callback through the HMAC-signed state token.
preserveConnectionFields copies fields from the existing connection
onto the new one when the new one omits them:
- OAuth2 refresh_token: Google drops it on incremental-auth reuse
when prompt=consent is skipped.
- Slack webhook URL, channel and channel ID: access review Slack
reconnects without the incoming-webhook scope return a token
response with no incoming_webhook field.
GetByOrganizationIDAndProvider now routes through the widest-scope
coredata loader, and GetWithConnection exposes a by-ID load that
returns the fully decrypted connector so the initiate handler can
read the stored scope set.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add an InitiateOptions struct to the Connector interface so each
caller can declare the scopes it needs instead of having them baked
into the connector at registration. The HTTP handler reads repeated
?scope= query parameters from /connectors/initiate and forwards them.
Also restore GOOGLE_WORKSPACE and LINEAR provider definitions which
were silently dropped from the bootstrap config refactor.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Use the ContinueURL from the state token so the user is redirected
back to where they initiated the flow instead of the root URL.
The redirect is safe because safeRedirect validates the host.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
When a provider returns an error (e.g. user denies consent), the
callback now logs the error with provider name and redirects to
the base URL with error and error_description query parameters
instead of falling through to the code exchange.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Reduce closure size in NewMux by extracting the /connectors/complete
handler into a dedicated handleConnectorComplete function. Cache
r.URL.Query() into a local variable to avoid repeated parsing.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add queries, mutations, and types for access review
campaigns, access sources, access entries with decisions
and flags, connector provider info, and provider org
listing. Wire accessreview.Service into the Resolver.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
SafeRedirect previously matched against a single static host string,
so OIDC callbacks always fell back to the console instead of
redirecting back to compliance pages on custom domains. Refactor
AllowedHost into a dynamic AllowedHostFunc and wire a trust-service
lookup into the connect handler so custom domain hosts are accepted.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>