ISO audits often span a window distinct from certificate validity.
Store optional audit_start_date and audit_end_date on the audit
record and expose them through GraphQL, MCP, CLI, n8n, and console.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
E2E owners confirm email during setup so password re-login works,
so ID token claim assertions must expect email_verified true.
Signed-off-by: Émile Ré <emile@probo.com>
Unverified password identities were able to open sessions after
signing out. Reject sign-in with EMAIL_NOT_VERIFIED and add a
resend-confirmation flow so users can complete verification.
Signed-off-by: Émile Ré <emile@probo.com>
Drop the request-all shortcut so callers always name the
documents, reports, and files to request. TopBar Get Access
now only signs in; bulk selection is the multi-resource path.
Signed-off-by: Émile Ré <emile@probo.com>
Visitors could only request access to one document, report, or file at
a time. Add row checkboxes and a bottom selection toolbar to the
compliance portal documents page so a visitor can select several rows
and request access to all still-locked ones in a single round-trip.
Expose a selection-scoped requestAccesses mutation that forwards the
chosen id lists to the existing RequestPortalAccess service (one
transaction, one NDA/auth gate). The resolver loads and tenant-checks
every target before requesting so a foreign id is rejected before any
access row is written, and echoes the affected nodes so the client
flips each row to pending in place.
Add a styled Base UI Checkbox to the v2 kit, a local selection context
shared by the independent row fragments, and mirror the new selection
strings across all locales.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the separate Add Source operation with multi-select source
fields on create and update. Create passes accessReviewSourceIds to
the existing GraphQL input; update gains the same omittable field and
backend source sync so workflows can configure sources in one step.
Load source options from the organization in the n8n UI, and keep
surfacing INVALID errors when start fails for missing sources.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Registration.ExtraSettings was a single flat list, but the API-key and
client-credentials connect dialogs need different fields whenever a
provider offers both paths, because a different create resolver and a
different driver sits behind each. Replace it with
APIKeyExtraSettings and ClientCredentialsExtraSettings, and split the
GraphQL surface to match so a client cannot render one path's settings
on the other.
This fixes two connectors that could not be connected at all.
1Password declared accountId and region only, which are the
client-credentials shape. The API-key dialog therefore rendered those
two fields, mapAPIKeyExtraSettingToField returned nil for both so
buildExtraFields discarded them, and the SCIM-bridge driver failed on an
empty SCIMBridgeURL. The console already mapped scimBridgeUrl, but no
registration declared that key, so the branch was dead. It now declares
scimBridgeUrl on the API-key path and accountId + region on client
credentials.
Langfuse declared baseUrl as required, but mapAPIKeyExtraSettingToField
had no LANGFUSE case, so buildExtraFields dropped the value the customer
typed and the mutation failed with "langfuseBaseUrl is required". Every
other extra-settings provider had a case. The GraphQL input field, the
settings struct, the probe builder and the driver were all already
correct; only the console mapping was missing.
buildExtraFields now takes the settings list explicitly instead of
reading it off the provider, so each dialog passes its own path's list
and cannot silently iterate the other one.
Register rejects a settings list for a path the provider does not offer,
and an empty or duplicate setting key within one list. A key repeated
across the two lists is allowed: that is how a dual-path provider
declares a setting both dialogs need.
The new resolver tests walk the whole chain the console walks, from the
key a Registration declares through the mutation input field to the
persisted settings struct, so a key renamed on one side and not the
other fails in CI instead of at connect time.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
When category was omitted from an MCP, GraphQL, or CLI update payload,
ThirdPartyService.Update overwrote the stored category with OTHER. Only
apply category when the request explicitly includes it, matching other
optional fields on the same update path.
Add an MCP e2e test that updates name without category and asserts the
existing category is unchanged.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
viewer.enrolledDevices shared DeviceConnection with the admin
org list, so totalCount had to authorize with both
employee-device:list and device:list. Mirror
EmployeeDocumentConnection: a dedicated EmployeeDeviceConnection
without totalCount, and keep DeviceConnection.totalCount for
the org fleet only. Cover assumed-session device:get IDOR in e2e.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
The /enroll wait UI polled device state via node(), which
requires an assumed org session, so confirmation never
succeeded for unassumed viewers. Expose viewer.enrolledDevice
behind itam:employee-device:get (own-device, skip assumption)
and point the poller at it.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
The access review Add Source dialog listed each connector with no
path to its setup documentation. Connectors that have a published
docs page on probo.com now surface a "Documentation" link on the
card, opening the page in a new tab; connectors without a page show
nothing extra.
The link is data-driven from the connector registry: a new
DocumentationURL on the provider Registration, populated for the 12
documented providers via a single accessReviewDocsURL helper, is
surfaced as a nullable documentationUrl on ConnectorProviderInfo and
rendered by the console only when present. This keeps the registry
the single source of truth and adds no client-side provider map.
The links resolve once the probo.com access-review docs pages are
deployed; until then they 404, so deploy the docs alongside this
change.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Store a short entity name instead of the full home
heading so orgs can brand portals for sub-entities.
Restore hero i18n composition and keep the English
document title composed from the entity name.
Signed-off-by: Émile Ré <emile@probo.com>
Rename trust center coverage to compliance
portal so console, MCP, and trust suites assert
against the new API surface.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Align console references and OAuth branding with the
compliance-page model, and fix certificate cache eviction,
portal OAuth handlers, and magic-link edge cases left after
the trust-center rename.
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>
Drop the portal-login redirect, accept continue URLs for magic links,
brand emails from the OAuth client when present, and update e2e coverage.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Update generated test config, loopback HTTPS client, and workflow
root CA wiring for the step-ca ACME directory and port-443 trust
center listener introduced by the new compose stack.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Extend test helpers for portal OAuth flows and cover connect,
callback, and NDA signing against the compliance portal API.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Store website, email, and headquarters on the trust center so
public and admin surfaces read branding from one place. Drop the
trust API organization type and wire console, MCP, CLI, and apps
through the updated schema.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Follow the new domain model in tests: drop organization profile
assertions, add a trust center profile test, and hit the dedicated HTTPS
listener with SNI for the visitor API. Mirror the custom link rename and
profile field moves in the n8n node operations.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Sync GraphQL commitment group and item operations
to the remaining API surfaces so automation can
manage compliance portal commitments end to end.
Signed-off-by: Émile Ré <emile@probo.com>
Archive and bulk archive now void pending approval quorums and
cancel requested signatures before freezing the document. Unarchive
reverts any leftover pending approval to draft. A migration backfills
archived documents that still carry in-flight workflow rows.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The MCP publishDocument tool required callers to pass approver_ids and
to distinguish an omitted list (rejected) from an empty one (direct
publish), a null-vs-empty subtlety that is awkward for an LLM to get
right.
Drop approver_ids from the MCP tool and resolve a major publish's
approvers from the document's default approvers instead: an approval is
requested when the document has default approvers, otherwise the version
is published directly. Default approvers are configured with addDocument
or updateDocument.
Replace a document's default approvers on every major publish that
supplies an explicit list, even when the list is empty, so a direct
publish through the GraphQL API clears stale approvers instead of
leaving them behind (previously the empty case skipped the update).
Expose the default-approver behaviour as a separate entry point,
PublishVersionWithDefaultApprovers, that loads the defaults and
delegates to PublishVersion. PublishVersion keeps its explicit-approver
contract for the GraphQL API.
Require only the publish permission to publish a version, whether or not
it opens an approval quorum, and drop the now-unused request-approval
action.
Fold the publish steps into the publishMinor and publishMajor primitives
shared by both the single and bulk publish paths, and drop the redundant
InTx suffix from RequestApproval and emitDocumentEvent, which already
take a transaction argument.
Signed-off-by: Sacha Al Himdani <sacha@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>
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>
Cover gaps the connectors shipped without: the Railway and Crisp name
resolvers had no tests despite real branch logic (single vs multiple vs
zero workspaces, best-effort on error, empty-website short-circuit,
tier header), and the crispVerificationCode query was only unit-tested
at the HMAC layer, never through the live schema and authorization
stack.
Add TestRailwayNameResolver and TestCrispNameResolver alongside the
existing resolver tests, pin the Crisp driver's hardcoded MFA Unknown
and nil Active, and add an e2e TestCrispVerificationCode asserting the
code shape, determinism, organization binding, blank-input INVALID, and
viewer FORBIDDEN. The verification-code query needs no Crisp credentials
(only the always-set token secret and organization authorization), so it
runs against the default e2e deployment.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Two files declared activateTrustCenter in package trust_test, so the
package no longer compiled, and the trust-center lookup plus activation
logic was duplicated four ways across the suite.
Move lookupTrustCenterID and activateTrustCenter into a shared
helpers_test.go and route every call site through them, dropping the
inline copies in the logo and report-export tests.
Signed-off-by: Émile Ré <emile@probo.com>
Subprocessor filtering for the compliance portal happens in the backend
rather than the client. Add a SubprocessorFilter (query, category,
country) to the trust API's subprocessors connection, thread it through
the resolver and service, and extend the coredata ThirdParty filter with
category equality and country array membership. The connection stores the
filter so totalCount reflects the filtered set. Add e2e coverage for the
new filtering.
On the frontend, convert the page to a refetchable fragment whose filter
arguments are driven by URL-persisted, debounced toolbar state (category
and region selects plus a search field), populate the dropdowns from an
unfiltered facet selection, and offer to clear filters from the empty
state.
Signed-off-by: Émile Ré <emile@probo.com>
Any self-provisioned trust center visitor could accept another
visitor's NDA signature or inject audit-trail events into it by
supplying its GID, since AcceptSignature and RecordEvent trusted the
client-supplied signature ID without checking it belonged to the
caller (GHSA-22xj-f767-ppw6). SignerEmail/ActorEmail are always
derived from the verified session identity, never client input, so
comparing them against the signature's stored SignerEmail in
pkg/esign/service.go closes the hole at its root without touching the
resolver-level authorization already in place elsewhere.
Adds an e2e regression test that self-provisions two trust center
visitors through the real magic-link flow and confirms one cannot
touch the other's signature.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Replace the deny-based restriction on granting OWNER with role-scoped
allow policies so authorization fails closed: admins may create and
update memberships only when the assigned role is not OWNER, and the
absence of a target role no longer implies permission.
To keep console UI gating accurate without loosening the base grants,
the permission field gains an optional typed options argument
(PermissionOptionsInput) that forwards target_role into the dry-run
authorization. Only the two role-related console calls (create user,
update membership) pass it; the OWNER option stays hidden for admins via
the existing assignable-roles helper.
Add a non-regression test that an admin cannot promote a member to OWNER
while still being able to change members between non-owner roles.
An organization ADMIN could hard-remove members, including OWNERs, because
removeUser (connect and MCP) only checked the weaker iam:membership-profile:delete
gate. Authorize the owner-only iam:membership:delete instead, and expose the
source attribute on MembershipProfile so the owner grant's non-SCIM condition
can match.
Consolidate ownership-grant authorization into policy for both createUser and
updateMembership: each resolver passes the requested role as a target_role
attribute and ADMIN is denied granting ownership via deny-create-owner /
deny-promote-owner. target_role is distinct from resource.role, which is the
target's current role and guards editing existing owners. With no callers left,
the iam:membership-role:set-owner action (grant and OAuth2 scope) is removed.
Also pass the authorized scope through to the RemoveUser/CreateUser services,
gate the console Remove action on iam:membership:delete, and add regression
tests plus a changelog entry.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The public trust API derived its authorization scope from client-supplied
global IDs, so a visitor on one trust center could resolve nodes, export
audit-report PDFs, and read or mutate electronic signatures belonging to
another organization (cross-tenant access).
Every trust API resolver now derives its scope from the active compliance
page's organization via compliancepage.ScopeFromContext, so reads are always
confined to the page's tenant. Cross-tenant or unknown IDs surface as
not-found instead of leaking data or returning a 500. Active/presence is
enforced upstream by the id and presence middlewares.
esign's signature operations (GetSignatureByID, AcceptSignature, RecordEvent)
now take a caller-provided scope instead of deriving one from the requested
ID, so signature reads and mutations are tenant-scoped at the source. This
removes the need for a resolver-level authorization helper; RecordEvent also
verifies signature ownership within scope before recording, since the event
foreign key is not tenant-composite.
Adds e2e non-regression tests covering owning vs. foreign trust center report
export and the generic node(id:) resolver.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
An organization ADMIN could mint an OWNER membership via createUser, which
only gated iam:membership-profile:create and bypassed the owner-only
iam:membership-role:set-owner check that updateMembership already enforces.
Gate the requested role in both createUser entry points (connect resolver
and the MCP CreateUserTool) with an additional set-owner authorization when
the role is OWNER, mirroring updateMembership. Add a regression test that
locks the ADMIN/OWNER privilege boundary the fix relies on.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Deleting a profile still referenced elsewhere (for example as an
asset owner) surfaced an internal error. PostgreSQL reports ON DELETE
RESTRICT blocks as SQLSTATE 23001, not 23503; map both in profile
delete and propagate ErrProfileInUse through removeUser as CONFLICT.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Introduce a Resolver that owns env lookup and typed parsing for
probod-bootstrap. Env values prefixed with aws://<secret-id> are
fetched from AWS Secrets Manager (plaintext SecretString); each
secret ID is cached per run. Builder now takes a Resolver only.
Prefix every probod-bootstrap input with PROBOD_ so bootstrap config
does not collide with unrelated process environment (for example
AWS_* used by other tooling). Secrets Manager authentication uses
the standard AWS SDK default chain (AWS_REGION, IAM role, profile);
PROBOD_AWS_* vars configure S3 in the generated config only.
Update Helm deployment env names, GNUmakefile dev-config, Lima
provision, e2e testutil, compose.prod.yaml, and docs.
Deployments must rename bootstrap env vars to PROBOD_* (e.g.
AUTH_COOKIE_SECRET → PROBOD_AUTH_COOKIE_SECRET).
BREAKING CHANGE: all env vars are now prefixed by `PROBOD_`.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
When a new minor version is published, still-REQUESTED signature requests
on the previous version now move onto the newly published version, keeping
the same signature row so the notification schedule (count and last-notified
time) is preserved. SIGNED signatures are left untouched.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>