50 Commits

Author SHA1 Message Date
Émile Ré
4a276e3ef7 Split inactive profile state
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>
2026-07-30 09:19:42 +02:00
Sacha Al Himdani
f604c48686 Archive SCIM users with in-use profiles instead of 500ing
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>
2026-07-15 15:44:29 +02:00
Sacha Al Himdani
944bcb7380 Add updated-from entity snapshot to updated webhooks
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>
2026-07-15 13:59:41 +02:00
Sacha Al Himdani
4c57d201a4 Make license declarations consistently MIT
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>
2026-07-13 16:21:14 +02:00
Sacha Al Himdani
9ab8ea2085 Refacto load all functions
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>
2026-06-16 14:35:16 +02:00
Sacha Al Himdani
9ac71f948f Update contact email to hello@probo.com
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-09 16:45:23 +02:00
Sacha Al Himdani
50c5454681 Deactivate SCIM users when delete is blocked
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>
2026-06-01 11:39:50 +02:00
Cursor Agent
f7309c642a Cancel signature requests on deactivation
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>
2026-05-28 14:58:01 -07: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
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
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
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
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
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
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
76f781ead1 Clear conflicting external_id when enrolling manual profile into SCIM
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>
2026-03-14 12:40:26 +01:00
Sacha Al Himdani
d5c62a9383 Run go fmt/fix
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-13 14:48:13 +01:00
Bryan Frimin
f99b2f7828 Rename nickname
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:03 +01:00
Bryan Frimin
e393411333 Move kind to string
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:02 +01:00
Bryan Frimin
764f61bfa7 Remove debug logs
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:01 +01:00
Bryan Frimin
dd3885ac54 Fix MS365 attributes handling
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:01 +01:00
Bryan Frimin
c6c6e29d73 Fix delete does not completly remove the record
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:01 +01:00
Bryan Frimin
346205f05a Returns 409 when user already exist
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:00 +01:00
Bryan Frimin
85663d3a34 Fix delete user membership instead of profile
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:00 +01:00
Bryan Frimin
c8250f6794 Add userName and externalID support
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-11 13:09:00 +01:00
Sacha Al Himdani
6905472fba Add user webhooks
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-02-27 10:14:53 +01:00
Émile Ré
c3a65c123a Sync title in SCIM and Gbridge
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:13:05 +04:00
Émile Ré
c74e7e23f6 Inline if block
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:13:04 +04:00
Émile Ré
ae0119e2c5 Expire invitations when user is deactivated or switches to SAML source
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:13:00 +04:00
Émile Ré
47aba96b69 Reimplement invitations
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:12:58 +04:00
Émile Ré
7794563e94 Remove duplicate membership method
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:12:56 +04:00
Émile Ré
56ec0ab3c3 Move source and state from membership to profile
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:12:55 +04:00
Émile Ré
108e9cb85d Remove coredata.MembershipProfile MemerhipID field
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-19 16:12:52 +04:00
Émile Ré
a3b49db43e Add organization_id on profiles and implement ListProfiles
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-12 16:21:11 +04:00
Émile Ré
6328952c6f Load identity email_address on coredata.MembershipProfile
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-02-12 16:21:09 +04:00
Bryan Frimin
055a606747 Fix active state not used at creation
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-02-11 16:03:44 +01:00
Bryan Frimin
1bc5ebce2c Fix SCIM disable all non SCIM managed members
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-02-03 16:01:03 +01:00
Bryan Frimin
3d4b215b8f Add bridge backend for sync
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-02-02 09:14:39 +01:00
Émile Ré
bbdea575d1 Expire invitations on SAML sign up and invitation accept
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-01-17 12:34:03 -08:00
Émile Ré
b790ca312a Expire invitations on SCIM delete
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-01-17 12:34:03 -08:00
Émile Ré
b961069e1d Accept/Expire invitations on SAML & SCIM operations
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-01-17 12:34:02 -08:00
Bryan Frimin
89439da20d Use employee as default role
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:30:30 -08:00
Bryan Frimin
c4285cdc42 Fix re-activitating membership does not reset the role
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:30:30 -08:00
Bryan Frimin
7030d06aed Add membership state
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:47 -08:00
Bryan Frimin
da8a670a68 Improve scim event UI/UX
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:46 -08:00
Bryan Frimin
d72024be1e Remove useless User struct
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:46 -08:00
Bryan Frimin
e2225d9970 Refactor scim filter management
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:46 -08:00
Bryan Frimin
eb4024d878 Simplify SCIM types/error management
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:45 -08:00
Bryan Frimin
3e9df307b8 Remove email type
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:45 -08:00
Bryan Frimin
31f79fccce Add SCIM handler draft
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-01-17 12:22:44 -08:00