The cookie banner's cross-origin POST was blocked by two layered issues:
1. The global cors.Handler (with OptionsPassthrough: false) intercepted
OPTIONS preflights before the cookie banner's own CORS middleware
could run. Customer website origins aren't in AllowedOrigins, so the
preflight response had no CORS headers. Move the cookie banner mount
outside the global CORS group since it handles CORS per-banner.
2. The CSRF bypass patterns used literal "*" instead of ServeMux wildcard
syntax "{rest...}", so they never matched real request paths like
POST /cookie-banner/v1/{bannerID}/consents. Also remove redundant
GET/OPTIONS bypass patterns since safe methods are always allowed.
Signed-off-by: Émile Ré <emile@getprobo.com>
Remove assets from the snapshot system and replace with a publish-based
document workflow that generates versioned ProseMirror documents.
- Remove snapshot_id/source_id from asset and asset_vendor models
- Delete AssetFilter (no longer needed without snapshot filtering)
- Add PublishAssetList service, GraphQL mutation, MCP tool, CLI command,
and n8n operation
- Add asset_list_document_id column to generated_documents table
- Generate ProseMirror documents with asset inventory tables
(name, type, amount, data types stored, owner, vendors)
- Add AssetListDocument resolver on Organization type
- Update frontend to remove snapshot routes/params and add publish dialog
- Add e2e tests for asset publish (immediate, with approvers, reuse, RBAC)
- Add migration script for converting legacy asset snapshots to documents
- Exclude ASSETS from snapshot type lists and e2e snapshot tests
- Move generated_documents SQL to coredata methods on Datum and Asset
- Clear generated document and SOA references on soft delete and archive
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The OAuth2/OIDC server accepted its signing key via a file path
(key-file), while every other PEM key in the probod config (SAML
private key, ACME account key) is embedded inline. Switch the
field to a private-key string so the convention is uniform.
The signing key is operator-supplied material that must outlive
any process restart, so the bootstrap builder now treats
OAUTH2_SERVER_SIGNING_KEY as required and refuses to start
without one; silently minting a fresh key per boot would break
token validation across rollouts. The OAUTH2_SERVER_* env vars
otherwise flow through builder.Build like the existing SAML
block so the new OAuth2Server section is populated end-to-end.
Rework the e2e harness to render its config via bootstrap at
test setup, which removes the static
e2e/console/testdata/config.yaml and the previously generated
test-only PEM file. A per-run RSA key is minted via
bootstrap.GenerateOAuth2SigningKey (kept public for test
tooling) and injected through the builder env map. CI now
passes ACME_ROOT_CA inline instead of mutating a YAML on disk.
Signed-off-by: Bryan Frimin <bryan@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>
Address PR review comments: capture Exec result and check
RowsAffected() == 0 to return ErrResourceNotFound in
Cookie.Update, CookieCategory.Update, and CookieCategory.UpdateRank.
Also update coredata and relay contributor docs accordingly.
Signed-off-by: Émile Ré <emile@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>
Drop the boolean implemented/not-implemented state in favor of a
mandatory CMMI maturity level enum (NONE, INITIAL, MANAGED, DEFINED,
QUANTITATIVELY_MANAGED, OPTIMIZING) stored as a Postgres enum type.
The migration backfills existing rows (NOT_IMPLEMENTED → NONE,
IMPLEMENTED → INITIAL), makes the column NOT NULL, and drops the old
implemented column and its enum type.
- maturityLevel is required on CreateControlInput and non-nullable (!)
in the GraphQL schema
- CLI displays human-readable CMMI labels instead of raw enum tokens
- SOA table and published document use a single Maturity column in
place of the old Implemented + Maturity columns
- Remove ControlImplementationState type and all implemented references
across backend, frontend, CLI, MCP, n8n, and E2E tests
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Adds an optional CMMI 0-5 maturity level field to Control to support
ISO 27001 clause 9.1 effectiveness measurement and HITRUST CSF maturity
requirements. The field is nullable, framework-agnostic, and exposed
across all four API surfaces (GraphQL, MCP, CLI, n8n) plus the
generated SoA document.
Signed-off-by: Alejandro Juan <alejandrojuan@alejandrojuan.com>
Moving a cookie between categories previously required two sequential
updateCookieCategory mutations, which was not atomic and could leave
data in an inconsistent state if the second call failed. This adds a
dedicated moveCookieToCategory mutation that performs both updates in
a single transaction.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the `required` boolean column on cookie_categories with a `kind`
enum (NORMAL, NECESSARY, UNCATEGORISED). The Necessary category remains
undeletable and always-on for consent; the new Uncategorised category is
also undeletable but users can opt out of it.
When a category is deleted, its cookies are merged into the Uncategorised
category (lazy-created for legacy banners that don't have one yet).
Signed-off-by: Émile Ré <emile@getprobo.com>
Parallel gqlgen instances race on the Go build cache, causing
Rewriter.getSource() to panic with "slice bounds out of range"
when go/packages returns empty source. Chain gqlgen targets with
order-only prerequisites in the Makefile and switch the CI build
job to sequential generation.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Category reordering previously required two separate
updateCookieCategory calls to swap ranks, which was not
atomic. Replace with a single reorderCookieCategory mutation
that shifts all affected ranks in one SQL statement, and
remove the rank field from UpdateCookieCategoryInput.
Signed-off-by: Émile Ré <emile@getprobo.com>
Drop meetings and meeting_attendees tables, remove all meeting-related
code across GraphQL, MCP, CLI, N8N, webhooks, frontend, and e2e tests.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Delete duplicate connectors per (organization_id, provider), keeping the
oldest one, then add a unique index to prevent future duplicates.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Mirror the SOA-to-document migration for the data list. Remove data
from the snapshot system and add a publish workflow that generates a
ProseMirror document for the full organization data inventory.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Expose document version approval quorums and decisions
through MCP, CLI, and n8n. This lets users inspect who
approved or rejected a document version, including the
rejection comment, without relying solely on the audit
log.
MCP tools: listDocumentVersionApprovalQuorums,
getDocumentVersionApprovalQuorum,
listDocumentVersionApprovalDecisions,
getDocumentVersionApprovalDecision.
CLI commands: document list-approval-quorums,
view-approval-quorum, list-approval-decisions,
view-approval-decision.
n8n operations: Get/Get Many Approval Quorums and
Approval Decisions on the Document resource.
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>
Expose webhook subscription CRUD and event listing through the MCP API
(list, get, create, update, delete subscriptions + list events) and add
a new webhook resource to the N8N node with matching operations.
Signed-off-by: Sacha Al Himdani <sacha@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>
Helper for callers that need to know whether a given connector row is
still referenced by any access_sources. Used by the SCIM disconnect
flow, which would otherwise fail with a foreign-key violation when the
connector is shared with an access source.
Mirrors the shape of CountByOrganizationID.
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>
Use typed ErrDocumentVersionNotPublished instead of plain fmt.Errorf in
signature request methods, and add missing ErrResourceNotFound handling
across document resolvers that were returning Internal for expected errors.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Statements of Applicability are no longer exported as one-off PDFs.
Instead, each SOA owns a persistent document that accumulates versions
over time, following the same publish/approve lifecycle as authored
documents.
Publishing without approvers publishes immediately; publishing with
approvers creates a draft pending approval via the existing quorum
system. SOAs can also store default approvers that are pre-populated in
the publish dialog.
The SOA is removed from the snapshot system — applicability statements
are now queried directly (snapshot_id IS NULL) rather than through
snapshot copies.
A standalone migration script (cmd/migrate-soa-snapshots-to-documents)
converts existing SOA snapshots into documents with proper ProseMirror
content, preserving version history and approval decisions.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace the draft-only unique index with one covering both DRAFT and
PENDING_APPROVAL statuses, ensuring a document can have at most one
active version at a time. Deduplicate any existing data before creating
the index, prioritizing PENDING_APPROVAL over DRAFT.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Merge type-only schema files (country_code.graphql, pagination.graphql,
identity.graphql) back into base.graphql for both trust/v1 and console/v1.
These standalone files had no corresponding _resolvers.go files, causing
gqlgen v0.17.87's Rewriter.getSource() to panic with 'slice bounds out
of range' when running go generate. By consolidating them into
base.graphql (which already has base_resolvers.go), gqlgen can process
the schema without needing separate resolver files for pure-type
definitions.
Add server-side validation in BulkRequestSignatures, RequestSignature,
and RequestApproval to load the referenced profiles and verify none
have an ended contract before proceeding. Returns ErrProfileContractEnded
if a profile's contract_end_date is in the past, surfaced as a CONFLICT
GraphQL error in all three resolvers.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Align MCP and n8n document listing endpoints with the GraphQL console
API so every document filter is available across all three interfaces.
MCP:
- listDocuments: add status filter, default to ACTIVE
- listDocumentVersions: add statuses filter
- DocumentOrderField: add UPDATED_AT
n8n:
- document getAll: add query, documentTypes, classifications, status filters (default ACTIVE)
- document getAllVersions: add statuses filter
- document getAllSignatures: add states and activeContract filters
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Strip forwarded headers (Forwarded, X-Forwarded-For, X-Real-Ip)
from requests originating from untrusted proxies at the HTTP
server level, reusing the existing proxy-protocol trusted-proxies
config. The clientip package is now a pure extraction helper;
context plumbing and middleware wrappers are removed.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add coredata guide section on using Go enum constants as named
SQL parameters instead of hardcoded string literals. Fix mixed
inline/multiline RenderJSON call in cookie banner handler.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move clientIP extraction into a reusable pkg/server/api/clientip
package with RFC 7239 Forwarded header support. Add
pkg/server/jsonutil with helpers for common HTTP error responses
(RenderForbidden, RenderInternalServerError, RenderNotFound,
RenderBadRequest) and use them in the cookie banner handlers.
Signed-off-by: Émile Ré <emile@getprobo.com>
Parse only the first IP from X-Forwarded-For to prevent
the full chain from bypassing anonymization. Add secondary
sort key for deterministic consent selection. Reject origins
with empty port suffix in the validator.
Signed-off-by: Émile Ré <emile@getprobo.com>
Implement config, consent retrieval, and consent recording
endpoints for the JS SDK. IP addresses are anonymized (last
octet zeroed for IPv4, /48 mask for IPv6) before storage.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce /cookie-banner/v1/{bannerID}/config endpoint for the JS SDK.
The custom CORS middleware validates each request origin against the
specific banner being requested, preventing cross-customer leakage.
Signed-off-by: Émile Ré <emile@getprobo.com>
Strip www. prefix and trailing slash from origin when creating or
updating a cookie banner so CORS lookups match regardless of whether
the customer's site redirects www to the apex domain.
Signed-off-by: Émile Ré <emile@getprobo.com>