Commit Graph

240 Commits

Author SHA1 Message Date
Bryan Frimin
6eb30c7b79 Fix timing attack on signin
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-25 23:04:44 -07:00
Bryan Frimin
bd04f1812a Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 14:07:37 -07:00
Bryan Frimin
1eba43f96f Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:37:33 -07:00
Bryan Frimin
b39d880c0f Switch AuthorizationAttributes to batch and add AuthorizeBatch
Change AuthorizationAttributer.AuthorizationAttributes to take a slice
of resource ids and return policy.AttributesByID, so a single SQL
round-trip can load condition attributes for a whole batch. All
coredata implementations are migrated to a single
`WHERE id = ANY(@resource_ids::text[])` query that returns only the
rows it finds.

Authorizer gains:
  - AuthorizeBatch — all-or-nothing across a homogeneous (same entity
    type, same organization) resource set; rejects mixed entity types,
    mixed organizations, and empty batches with structured errors.
  - AuthorizeMulti — heterogeneous evaluation that returns one error
    per item and writes audit log entries in a single bulk insert.

The single-resource Authorize is rewired to delegate to AuthorizeBatch
so all paths share the same condition evaluation and audit logging.
recordAuditLog is split into buildAuditLogEntry plus a batch insert.

Tests cover the new batch and multi paths, mixed/empty/unsupported
resource cases, audit log batching, and dry-run behaviour.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:36 -07:00
Bryan Frimin
2d08dfbe2f Add batch authorization error types
Introduce ErrMixedOrganizationBatch, ErrMixedEntityTypeBatch,
ErrEmptyResourceBatch, and ErrBatchAuthorizationUnsupportedResourceType
along with their constructors. These errors will be raised by the
upcoming AuthorizeBatch path and carry enough structured fields for
GraphQL/MCP wrappers to map them to user-facing error codes.

Cover their Error() formatting alongside the existing single-resource
authorization errors.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:35 -07:00
Bryan Frimin
9b6bee4a27 Introduce policy.Attributes and policy.AttributesByID aliases
These aliases (`map[string]string` and `map[gid.GID]Attributes`) give
batch authorization call sites readable types when loading and
returning per-resource condition attributes. ConditionContext now uses
the alias instead of the bare map type, with no behavior change.

Also extend policy tests to cover ResourcePattern.MatchesResource,
comma-separated value handling for In/NotIn, unresolved-reference
fallthrough, and resolveKey/resolveValue.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:35 -07:00
Bryan Frimin
4d1a98ffdb Create scope in Authorize
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:34 -07:00
Émile Ré
f5703d390b Enforce Go style rules across codebase
Apply five style rules: convert iota string enums to typed
string constants, replace errors.As with errors.AsType,
merge three-group imports into two groups, fix multiline
parameter/argument formatting, and replace fmt.Sprintf URL
construction with net/url.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 11:46:39 +04:00
Émile Ré
f3cc159f1f Remove useless error checks
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 09:53:09 +04:00
Émile Ré
9156d6a16a Add wsl linter and fix
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 09:27:28 +04:00
Sacha Al Himdani
eecbe4c46c Rename vendors to third parties
Renames the user-facing 'vendor' concept to 'third party' across the
entire codebase. The shared common_third_parties reference table is
unchanged.

Migration. Renames the vendor_category enum, the vendors and
vendor_<entity> tables (contacts, services, compliance_reports,
business_associate_agreements, data_privacy_agreements,
risk_assessments) and their vendor_id columns, the asset_vendors /
data_vendors / processing_activity_vendors junction tables,
generated_documents.vendors_document_id, the webhook_event_type
'vendor:<verb>' values, and the snapshots_type 'VENDORS' value.

Backend. Renames coredata models and SQL queries, probo services,
GraphQL / MCP API surface, console / trust / webhook resolvers and
types, the CLI (prb vendor* -> prb third-party*; pkg/cmd/vendormgmt
-> pkg/cmd/thirdpartymgmt), the document generator, vetting agent
prompts, and the common-third-parties-import command.

Frontend, packages, n8n, e2e. Renames apps/console pages, components,
hooks, routes, dialogs, and tabs; the shared @probo/vendors package
(now @probo/third-parties); the @probo/ui Vendors atoms (now
ThirdParties, VendorLogo -> ThirdPartyLogo); the n8n community node
actions/vendor folder (now actions/thirdParty); and the e2e Go test
suite (console and MCP). Filesystem and URL paths use kebab-case
(third-parties), GraphQL fields and TypeScript identifiers use
camelCase (thirdParty / thirdParties), Go types use PascalCase
(ThirdParty), and human-facing text uses 'third party' with a space.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-13 21:21:39 +02:00
Sacha Al Himdani
a67f2433b1 Address code review findings in relay fetch and iam
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-12 18:42:54 +02:00
Émile Ré
d40e9a157b Handle membership not found
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-05-12 18:25:44 +04:00
Bryan Frimin
cf53ca3a0d Fix Microsoft 365 SCIM bridge connection and scope
Add the missing MICROSOFT_365 value to the connector_provider enum
so the connector loader stops failing with SQLSTATE 22P02.

Scope each Identity Provider card to its own SCIMBridge type so
connecting Microsoft 365 no longer marks Google Workspace as
connected (and vice versa).

Filter Microsoft Graph /users to userType eq 'Member' so the bridge
only syncs home-tenant members and skips B2B guest accounts that
were polluting the synced People list.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-07 18:01:24 +04:00
Bryan Frimin
c00711360c Support Google Cloud Identity in SCIM bridge
The SCIM bridge requested admin.directory.userschema.readonly during
OAuth consent, which is a Google Workspace-only entitlement. Cloud
Identity-only admins could not grant it, so the connect flow failed
before any sync ran. The scope was also unused: the provider only
calls Users.List, never the schemas, groups, or customers endpoints.

Trim the requested scopes down to admin.directory.user.readonly so
the integration works for Workspace and Cloud Identity (Free and
Premium) tenants. Switch Users.List to projection=full so standard
extended fields (Organizations, ExternalIds, Relations, Languages)
are populated on synced users; full projection does not require any
extra OAuth scope. Relabel the connector UI to "Google Workspace /
Cloud Identity" to reflect the broader support.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-07 09:41:46 +02:00
Bryan Frimin
5e55c888c4 Add Microsoft 365 SCIM bridge and access review driver
Microsoft 365's native SCIM endpoint is unreliable, so mirror the
Google Workspace bridge over Microsoft Graph: a new MICROSOFT_365
OAuth2 connector, a SCIM bridge provider listing /v1.0/users with
$select pagination, and an access review driver that derives admin
status from /directoryRoles members. Refactor the bridge runner to
share OAuth2 plumbing across providers and surface the new bridge
type, scopes, UI card, and bootstrap env wiring.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-06 15:41:30 +02:00
Bryan Frimin
febf4a930e Treat OIDC and magic link sessions as password-equivalent when assuming an org
Users authenticated via Google/Microsoft OIDC or magic link previously
relied on a fall-through in the password-only org check. Make the rule
explicit so SSO-only users can access password-only organizations
without being bounced to the password login form they cannot satisfy.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-30 17:24:04 +04:00
Bryan Frimin
79d4730806 Drop IntrospectToken doc comment
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:59:11 +04:00
Bryan Frimin
2418079785 Introspect OAuth2 refresh tokens
RFC 7662 lets clients introspect any OAuth2 token, but the endpoint
only resolved access tokens. Look up refresh tokens too, honor the
optional token_type_hint to drive lookup order with a fallback to the
other table, and report revoked or expired refresh tokens as inactive.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:59:09 +04:00
Bryan Frimin
62f05b3ff2 Invalidate sessions on password change and reset
Session validity was decoupled from credential rotation: changing
a password (logged-in flow) or completing a forgot-password reset
left every existing iam_sessions row valid until its idle TTL.
A user who saw their account compromised on another device had
no way to actually evict that device by rotating the password.

Inside the same DB transaction as the password update, expire the
identity's other active sessions:

- ChangePassword keeps the caller's current session and revokes
  every other session for the identity, so the user is not
  logged out of the browser they just used.
- ResetPassword has no caller session (the user is anonymous,
  authenticated only by a stateless token) and revokes all of
  the identity's active sessions.

The session middleware already rejects rows with expire_reason
set, so revoked sessions are kicked out on the next request
without any middleware change.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:57:19 +04:00
Sacha Al Himdani
e5e17d59ac Handle SCIM user email rename via external ID fallback
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>
2026-04-23 10:45:48 +02:00
Sacha Al Himdani
dfde600e5c Include external ID in SCIM bridge user creation error
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-22 16:45:30 +02:00
Sacha Al Himdani
a622c610d7 Fix SCIM bridge PUT loop and pagination
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>
2026-04-21 16:54:20 +02:00
Bryan Frimin
f743c743c1 Only guard owner demotion for active profiles
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>
2026-04-21 11:28:43 +02:00
Bryan Frimin
dd8c3b5491 Prevent last owner from demoting themselves
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>
2026-04-21 11:28:42 +02:00
Bryan Frimin
11770b4058 Add OAuth2/OpenID Connect authorization server
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>
2026-04-19 12:00:53 +02:00
Aurélien Sibiril
f15936dcdf Preserve shared connector on SCIM disconnect
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>
2026-04-16 16:27:30 +02:00
Aurélien Sibiril
0a5dfc652f Use typed error when SCIM bridge is not found
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>
2026-04-16 16:24:09 +02:00
Sacha Al Himdani
06c0972551 Add membership and host to user webhook payload
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>
2026-04-13 17:14:01 +02:00
Sacha Al Himdani
76f6c8f9d1 Show registration unavailable page when signup is disabled
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>
2026-04-09 18:28:59 +02:00
Aurélien Sibiril
56c042b7ed Add OAuth2 scope sources for access review, slack, googleworkspace
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>
2026-04-08 11:05:12 +02:00
Bryan Frimin
7bee5f27d2 Fix SCIM sync
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-03 17:21:08 +02:00
Bryan Frimin
f17fb7bf49 Upgrade to kit v0.3.0
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-03 11:56:06 +02:00
Bryan Frimin
8f59eb6b8a Distinguish expired magic links from invalid tokens
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>
2026-03-27 09:15:41 +01:00
Bryan Frimin
d1e4f31d07 Fix empty search_engine_indexing on trust centers
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>
2026-03-26 10:13:00 +01:00
Sacha Al Himdani
8c02c53315 Update copyright headers across all Go files
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-25 17:38:32 +01:00
Bryan Frimin
e74c0a11c6 Update copyright headers to 2025-2026 for modified files
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-25 15:19:47 +01:00
Bryan Frimin
d24712344e Add GraphQL dataloaders for batched record lookups
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>
2026-03-25 15:19:46 +01:00
Bryan Frimin
63965261db Fix Microsoft OIDC token exchange auth style
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>
2026-03-25 09:43:33 +01:00
Bryan Frimin
728804b559 Style
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-25 09:40:44 +01:00
Bryan Frimin
dcf81c4575 Fix SCIM bridge updating all users on every sync
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>
2026-03-25 09:40:44 +01:00
Bryan Frimin
784c88895a Allow editing non-SCIM fields on SCIM-managed profiles
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>
2026-03-24 16:44:53 +01:00
Bryan Frimin
f8e086a00b Address PR review feedback for OIDC sign-in pages
- 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>
2026-03-23 14:17:44 +01:00
Bryan Frimin
7e757bfa06 Sort OIDC providers for deterministic API response order
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>
2026-03-23 14:17:44 +01:00
Bryan Frimin
95803e4bca Fix wg.Go call style in OIDC service Run method
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-23 14:17:44 +01:00
Bryan Frimin
29b41208f3 Address PR review feedback for OIDC login
- 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>
2026-03-23 14:17:43 +01:00
Bryan Frimin
83468db034 Refactor sign-in page and IAM service lifecycle
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>
2026-03-23 14:17:43 +01:00
Bryan Frimin
23084a72a2 Add OIDC login support for Google and Microsoft providers
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>
2026-03-23 14:17:43 +01:00
Bryan Frimin
2f8edfb6be Add per-email sender name for compliance page emails
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>
2026-03-22 10:56:51 +01:00
Bryan Frimin
3bb27fe8f1 Style
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-21 19:16:21 +01:00