When a user's email is renamed in the identity provider (e.g. Google
Workspace), the external ID stays the same but the email changes. The
SCIM CreateUser now falls back to external ID lookup when no profile is
found by identity, and reassociates the existing profile to the new
identity instead of failing with a 409 uniqueness error.
Also removes user emails from bridge sync error messages to avoid
logging PII, using external IDs instead.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Two bugs caused SCIM sync failures:
1. buildUserPayload conditionally omitted empty fields. When a field was
cleared in the identity provider, the PUT payload didn't include it,
so the SCIM handler never cleared the stored value. The bridge kept
detecting a mismatch every sync cycle, causing a perpetual PUT loop.
Fix: always include all fields unconditionally.
2. ListUsers ignored the startIndex parameter — the cursor always started
from nil, so every page returned the same first N users. Organizations
with more than 100 SCIM-managed users never got a full listing; users
beyond the first page appeared missing, causing CreateUser calls that
failed with 409 (uniqueness conflict) and eventually disabled the
bridge. Fix: replace cursor-based pagination with OFFSET/LIMIT to
honor SCIM's 1-based startIndex.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Move profile load before the owner-demotion guard and add
an active-state check, matching the RemoveUser pattern.
Without this, demoting an inactive owner would be
incorrectly blocked.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
The UpdateMembership path allowed the sole owner of an
organization to change their role to a non-owner role,
causing permanent lockout. Add the same active-owner count
guard already used in RemoveUser.
Closes#1071
Signed-off-by: Bryan Frimin <bryan@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>
DeleteSCIMConfiguration unconditionally deleted the underlying OAuth2
connector together with the SCIM bridge and config. When the same
connector was also referenced from access_sources -- which happens
when Google Workspace is used for both SCIM and access reviews -- the
access_sources.connector_id foreign key (NO ACTION) rejected the
DELETE, aborting the whole transaction. Nothing was deleted and the
resolver returned an INTERNAL error.
Check the access_sources reference count before deleting the connector
and skip the connector delete when it is still in use. The bridge's
own connector_id FK is ON DELETE SET NULL, so dropping the bridge
alone is sufficient to unbind SCIM; leaving the connector untouched
keeps the access source working.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
UpdateSCIMBridge returned bare fmt.Errorf("SCIM bridge not found")
strings on the two not-found branches (resource missing, and
cross-tenant mismatch). Every other call site returning the same
condition uses the typed NewSCIMBridgeNotFoundError(bridgeID) (see
lines 1962 and 2153 in this file).
Switch both branches to the typed error so the error shape is
consistent across the service and callers can use errors.As to
detect the condition.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Nest membership ID, role, and state into a membership sub-object
in the user webhook payload. Also emit user:updated webhook when
the membership role is changed. Add X-Probo-Webhook-Host header
to webhook HTTP calls. Skip delete webhook when membership is
not found in SCIM user deletion.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Instead of showing the signup form and returning an internal error on
submit, the SignUpPage now queries signUpEnabled upfront and displays a
friendly message explaining that registration is not available, with a
link back to login.
Adds a signUpEnabled GraphQL query field on the connect/v1 API and
handles ErrSignupDisabled as a FORBIDDEN error in the SignUp resolver.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Each module that initiates an OAuth2 flow now declares its scopes
in its own package instead of duplicating them in the frontend or
in shared connector config:
- pkg/accessreview/drivers: per-provider scopes for the access
review drivers
- pkg/slack: scopes for the compliance page integration
- pkg/iam/scim/bridge/provider/googleworkspace: scopes for the
SCIM provisioning bridge
These constants are surfaced to the frontend via GraphQL fields
so the frontend never hardcodes scope strings.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
When a magic link token expires, the user now sees a specific
error message ("This magic link has expired. Please request a
new one.") instead of the generic "Failed to connect" error.
This adds ErrExpiredToken to the IAM error types, checks for
statelesstoken.ErrExpiredToken in both GetMagicLinkEmail and
OpenSessionWithMagicLink, and handles it in the trust resolver
and frontend.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Trust centers were created without setting SearchEngineIndexing,
defaulting to an empty string in the database. This caused scan
errors when loading trust centers.
Add a migration to fix corrupted rows, set a DEFAULT on the
column, and add a CHECK constraint. Also set the field explicitly
when creating new trust centers.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Introduce dataloadgen-based dataloaders to batch individual
record-by-ID fetches in GraphQL resolvers into single SQL
queries. Each entity type (organization, framework, control,
vendor, document, risk, measure, task, file, report, profile)
gets a LoadByIDs method in coredata and a GetByIDs service
method with variadic arguments and dedicated collection return
types. Resolvers now use dataloader.FromContext instead of
direct service calls for single-record lookups.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Without an explicit AuthStyle, the oauth2 library uses
auto-detection which tries Basic auth first. Microsoft
rejects this, and since PKCE codes are single-use, the
retry with the correct style fails.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
The SCIM client User struct had json:"-" tags on most fields
(GivenName, FamilyName, ExternalID, Department, etc.), so
ListUsers never populated them from the JSON response. The
bridge comparison always saw empty strings on the SCIM side
vs actual values from the provider, making needsUpdate true
for every user on every sync cycle.
Add custom UnmarshalJSON on User to properly parse nested
SCIM JSON (name object, enterprise extension) into the flat
struct, so the existing diff logic correctly skips unchanged
users.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Contract start and end dates are never synced by SCIM, so they
should remain editable even when a profile is SCIM-managed.
The backend now skips overwriting SCIM-synced fields (fullName,
kind, position, additionalEmailAddresses) for SCIM profiles,
and the frontend disables only those fields instead of the
entire form.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
- Remove inline password form from SignInPage (use PasswordSignInPage)
- Extract Divider and OIDCButtons to _components folder
- Move OIDC providers into page queries instead of lazy-loaded queries
- Create useSafeContinueUrl hook for trust app using getPathPrefix
- Use safeContinueUrl.toString() for continue URL parameter
- Fix wg.Go style in IAM service Run method
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
EnabledProviders() iterated a map, producing nondeterministic order.
Sort the slice before returning to ensure stable UI and test behavior.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
- Remove sensitive nonce values from error messages to prevent logging leaks
- Guard ticker intervals against non-positive durations in SAML domain
verifier and garbage collector to prevent panics
- Require both client ID and client secret for Google/Microsoft OIDC
providers to be marked as enabled
- Replace http.DefaultClient with kit/httpclient for JWKS fetching to
ensure proper timeouts
- Fix eslint indentation in SignInPage OIDC button click handler
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Redesign the sign-in page to show email/password form inline
with OIDC provider buttons (with vendor icons) instead of
separate pages. Extract OIDCProvider type to its own file.
Replace errgroup with sync.WaitGroup + WithCancelCause for
graceful shutdown in IAM services. Refactor garbage collectors
to use functional options and time.Ticker instead of
time.After to avoid repeated allocations.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Implements OpenID Connect authentication flow with PKCE, JWT verification, and enterprise-only account restrictions. Adds OIDC service with JWKS caching and state management, HTTP handlers for login/callback flows, GraphQL query for available providers, and sign-in UI integration.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When an email has a sender name set (the organization name), the
mailer composes the From header as "OrgName via Probo" instead of
the default global sender name. This gives compliance page
recipients clearer context about which organization is contacting
them.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Adds audit logging that records all authorized actions performed by
users and API keys. The audit log is automatically populated whenever
the authorizer approves an action, and is queryable via GraphQL, MCP,
and CLI interfaces. Permission checks are excluded via a dry-run flag
to avoid phantom entries on page loads.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When creating an organization, the owner's membership profile was being set with the organization name as the full name instead of the owner's actual full name. Fetch the identity's full name and use it for the profile.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
- Hold SELECT FOR UPDATE lock within transaction by using tx directly instead of separate WithConn, ensuring mutual exclusion when multiple requests race to verify the same token
- Fix "resouce" → "resource" typo in authentication error messages
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Adds a visibility enum to files with PRIVATE (default) and PUBLIC states.
PUBLIC files are accessible via an unauthenticated /api/files/v1/{fileID}
endpoint that redirects to a presigned S3 URL. Introduces pkg/file service
to manage file operations. Logo uploads (trust centers, organizations,
frameworks, references) are marked PUBLIC; other files are PRIVATE.
Includes database migration and backfill for existing logos.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Expand mixed inline/multiline function calls so each argument
is on its own line, matching the one-argument-per-line rule.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
When a SCIM provider sends CreateUser for a user whose email matches an
existing manual profile, but another profile already holds that
external_id (e.g. created by a prior CreateUser with a different email),
clear the conflicting external_id before enrolling the manual profile.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>