Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.
Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.
A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.
Signed-off-by: Émile Ré <emile@probo.com>
When a SCIM hard delete targets a profile that is still referenced
(e.g. completed document version signatures, FK RESTRICT), profile.Delete
fails with 23503 and poisons the surrounding transaction. The existing
deactivate fallback then ran on the aborted transaction and failed with
25P02, surfacing to the connector as an opaque 500 and eventually
disabling the bridge.
Wrap profile.Delete in a savepoint so the FK violation only rolls back
the delete attempt, leaving the outer transaction healthy for the
deactivate/archive fallback. Also map FK violations in Membership.Delete
to ErrResourceInUse for consistency with MembershipProfile.Delete.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Update webhook events now carry a top-level "updatedFrom" field
alongside "data", containing a full snapshot of the entity as it was
before the update. This lets subscribers diff old vs new state (for
example the prior membership role on user:updated) without tracking
prior state themselves. It is a complete snapshot with the same shape as
"data", not a partial diff, so consumers select whatever fields they
need. The field is omitted for non-update events.
The webhook_data table gains a nullable updated_from JSONB column, and
webhook.InsertUpdateData enqueues both snapshots; InsertData delegates to
it with a nil updatedFrom so non-update callers are unaffected. Each
*:updated emission site snapshots the entity right after load, before
mutation: obligation, third-party, user (org and SCIM flows), document,
document-version, and document-version-approval-quorum. The document
emit helpers gained an optional updatedFrom argument threaded through to
the payload.
For document-version-approval-quorum:updated the snapshot requires an
extra query, so it is now gated behind the same subscription-existence
check the emitter uses: when no subscriber is configured the load is
skipped entirely rather than running (and potentially failing the
approval) for an event nobody receives.
Add integration tests (against a real Postgres, skipped when none is
reachable) covering the updated_from round-trip, the SQL NULL behavior
when no snapshot is provided, and the no-op when no subscription matches,
plus a unit test asserting updatedFrom is omitted from the payload when
absent.
Document the new field in the probod and n8n changelogs and the n8n
README.
Signed-off-by: Sacha Al Himdani <sacha@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>
Unbounded LoadAll* loaders materialised an entire result set in one
query with no ceiling. A table that is small in development can grow
without bound in production, so these loaders were a latent memory
and query-time hazard.
Remove the LoadAll* methods from pkg/coredata and walk the cursor-
paginated LoadBy* siblings instead through a shared page.LoadAll
helper. The helper advances a MaxCursorSize forward cursor until the
result set is exhausted and concatenates the pages. It caps a single
call at MaxLoadAllPages (20) batches of 500 rows and errors past that
rather than materialising an unbounded set, so a runaway caller fails
loudly instead of exhausting memory.
Callers that genuinely need every row now express that explicitly,
and the coredata load-naming rule and docs are updated to discourage
new unbounded loaders.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
SCIM DELETE returned 500 when a profile was still referenced
elsewhere in the org, which disabled the identity-provider bridge
after repeated sync failures. Fall back to deactivation when delete
is blocked, log the conflict without failing sync, and still
attempt delete for excluded users even when inactive.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Disabling a profile now removes pending document signature requests tied to that signatory. This keeps inactive accounts from retaining signature work across manual, archived, and SCIM deactivation flows.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>