Commit Graph

1720 Commits

Author SHA1 Message Date
Aurélien Sibiril
c7ec1bcab3 Validate Slack OAuth2 token response
Check the ok and error fields from Slack's token
response and reject flows with a missing access token.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
8d70cf1dc1 Stop leaking internal error in initiate handler
The 500 response was wrapping the underlying error with
%w, exposing internal details to the client. Log the
full error, return a generic message.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
c6a5d98755 Drop grouped brackets for single declarations
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
cc6d271f10 Remove unused ScopesCover function
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
1def3a4136 Remove unused request param from InitiateWithState
The r *http.Request parameter was not read inside
InitiateWithState — all test call sites passed nil.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
816b9300ed fix(slack): select widest-scope connector in sender
sendMessage and updateMessage used LoadAllByOrganizationIDProtocol
AndProvider and then picked connectors[0], which is ordered by
created_at ASC. On an organization with multiple Slack installs
only the oldest install ever received messages — re-installing
Slack to switch channels silently broke because the sort kept
the old row winning.

Switch to LoadOneByOrganizationIDAndProvider, which picks the
widest-scope row with updated_at DESC as the tiebreak. For Slack
the scope sets are typically identical across installs, so the
effective behavior becomes "most recent install wins" — the
expected behavior.

While here, prefix the remaining error messages in this file with
"cannot" to match the project convention.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
08f163f02f refactor(console): rewrite /connectors/initiate to union scopes
The initiate handler now looks up the existing connector for the
target (organization, provider) pair, reads its stored scope set
through Connection.Scopes, and unions it with the scopes the caller
passed in the query string. The union is what gets requested on the
OAuth authorization URL, so reconnects never drop a previously
granted scope.

When an existing connector is found the handler also flags the
flow as a reconnect via InitiateOptions.ConnectorID, so the
OAuth2 state carries the id and the callback updates the row in
place. When the provider supports it (Google Workspace), the auth
URL also carries include_granted_scopes=true and the user sees
only the delta on the consent screen.

There is no short-circuit: every initiate click runs the full
OAuth flow even if stored scopes already cover the request, because
scope coverage is an unsafe proxy for token liveness. Revoked
tokens or leftover connectors from deleted access sources would
otherwise be silently reused.

The handler body is extracted to its own file to keep NewMux
readable.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
5340953966 feat(probo): validate reconnects and preserve dropped token fields
Reconnect now takes a ReconnectConnectorRequest carrying the expected
OrganizationID and Provider. It validates inside the same transaction
that the loaded connector belongs to the requested org, provider and
OAUTH2 protocol before mutating the row. This blocks cross-org and
cross-provider corruption via a crafted connector_id reaching the
OAuth callback through the HMAC-signed state token.

preserveConnectionFields copies fields from the existing connection
onto the new one when the new one omits them:

- OAuth2 refresh_token: Google drops it on incremental-auth reuse
  when prompt=consent is skipped.
- Slack webhook URL, channel and channel ID: access review Slack
  reconnects without the incoming-webhook scope return a token
  response with no incoming_webhook field.

GetByOrganizationIDAndProvider now routes through the widest-scope
coredata loader, and GetWithConnection exposes a by-ID load that
returns the fully decrypted connector so the initiate handler can
read the stored scope set.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
6390b15807 feat(coredata): add widest-scope connector loader
LoadOneByOrganizationIDAndProvider returns the effective OAuth2
connector for an (organization, provider) pair. When legacy rows
leave multiple candidates behind (pre-auto-reconnect installs), it
picks the row with the widest stored scope set and tiebreaks by
most recent updated_at.

This is correct for Google Workspace where the SCIM bridge's
4-scope row must win over the access review's 2-scope row so the
caller always sees a token that can run SCIM operations. The
selector reads the decrypted scope set through Connection.Scopes,
so it is robust against token-refresh updated_at churn that would
mislead a pure SQL ORDER BY.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
71f6364df0 feat(connector): support OAuth2 scope preservation and incremental auth
Extend the OAuth2 connector so a reconnect can request the union of
previously granted and newly requested scopes without losing either.
Four related changes:

- Connection gains Scopes() []string so callers no longer need a type
  switch to reach the scope set. OAuth2Connection and APIKeyConnection
  implement it; SlackConnection inherits via embedding.
- OAuth2State carries RequestedScopes and CompleteWithState falls
  back to it when the provider omits the scope field (RFC 6749 §5.1
  allows this when granted equals requested). Without the fallback
  the stored Scope would be empty and the next reconnect would have
  no diff base.
- providerDefinition gains SupportsIncrementalAuth, set only for
  Google Workspace. When the flag is true and the caller passes
  InitiateOptions.IncludeGrantedScopes, the auth URL carries
  include_granted_scopes=true and the prompt=consent param is
  dropped so reuse flows see only the delta consent screen.
- InitiateOptions gains ConnectorID so the reconnect case is passed
  explicitly instead of relying on the caller to mutate r.URL.Query.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
62bab0f732 fix(connector): relax Slack incoming webhook requirement
ParseSlackTokenResponse treated the incoming_webhook field as
mandatory, which blocked any Slack OAuth2 flow that did not request
the incoming-webhook scope. Access review Slack connects only ask
for users:read and users:read.email and would fail at token parsing.

Treat incoming_webhook as optional: populate SlackSettings when it
is present, leave them empty otherwise. The existing compliance-page
webhook URL is preserved through Reconnect in a later commit.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
d0fedcf39e feat(connector): add scope parsing utilities
ParseScopeString, FormatScopeString, UnionScopes and ScopesCover
encapsulate OAuth2 scope-set arithmetic. ParseScopeString accepts
both the RFC 6749 space-separated form and GitHub's comma-separated
non-compliant form in one pass, so callers can stay format-agnostic.

These primitives are the foundation for scope-preserving reconnect:
later commits compute the union of stored and requested scopes so
a reconnect never drops a previously granted scope.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
91eb41bd15 Add Slack access review scopes
Slack has two OAuth2 use cases in the app: the compliance page
integration (chat:write, channels:join, incoming-webhook) and the
access review driver that lists workspace members via users.list
(users:read, users:read.email). The per-caller scope refactor
already handled the compliance page but missed the access review
path, leaving it silently broken (zero scopes → missing_scope from
Slack API).

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Aurélien Sibiril
f65b6e8cfb Add go:generate directive for genmodels
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:11 +02:00
Aurélien Sibiril
481b56402a Derive provider from model ID
The provider is always the prefix before "/" in the model ID,
so storing it as a separate field is redundant. Replace the
field with a Provider() method.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:11 +02:00
Aurélien Sibiril
b4910cf511 Use controlled test data in registry tests
Build registries from inline map literals instead of relying
on generated OpenRouter data that can change between runs.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Aurélien Sibiril
02c719dfdd Use map for generated model registry data
Replaces []ModelDefinition with map[string]ModelDefinition so
model ID uniqueness is enforced by the data structure itself
and lookups do not require a linear scan.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Aurélien Sibiril
3f4ef7b104 Emit all supported parameter flags explicitly
Always write both true and false values in the generated
registry so unsupported parameters are visible at a glance
instead of relying on Go zero-value defaults.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Aurélien Sibiril
3365171672 Add model registry tests
Lookup by canonical ID, bare name, and normalized form.
Capability assertions for Claude, GPT-5.4, and o3. Edge
cases for empty string and provider-prefix-only inputs.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Aurélien Sibiril
c9ba7510c5 Add generated model registry data
177 models across 9 providers fetched from OpenRouter.
This file is generated by go generate and committed to avoid
requiring an API call at build time.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Aurélien Sibiril
45d9a9e43b Add model registry types and lookup
ModelDefinition, SupportedParameters, and Registry types with
multi-key lookup supporting canonical, bare, and normalized
model IDs. NewRegistry constructor accepts model definitions
for testability; DefaultRegistry caches the generated data.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 18:25:10 +02:00
Sacha Al Himdani
eeee93e464 Drop legacy document columns and approver tables
The title, classification, and document_type columns have been moved to
document_versions. The document_approvers and document_version_approvers
tables have been replaced by approval quorums and decisions.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-13 18:10:14 +02:00
Sacha Al Himdani
d40b114f0f Add document resource to n8n node and MCP sendSigningNotifications tool
Add a complete document resource to the n8n node with 21 operations
covering documents, versions, and signatures — matching the MCP
specification. Also add the sendSigningNotifications tool to the MCP
API for triggering pending signature reminders.

n8n operations: create, get, getAll, update, delete, archive,
unarchive, getVersion, getAllVersions, createDraftVersion,
updateVersion, deleteDraftVersion, publishMajorVersion,
publishMinorVersion, requestApproval, voidApproval, getSignature,
getAllSignatures, requestSignature, cancelSignature,
sendSigningNotifications.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-13 17:31:06 +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
0826f28867 Fix missing fields in MCP type serializers
Framework was missing OrganizationID, Task was missing MeasureID and
AssignedToID, Asset was missing SnapshotID, AuditLogEntry was missing
Metadata, and Obligation was missing SourceID. All these fields were
defined in the MCP generated types but never set by their converters.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-13 14:40:47 +02:00
Sacha Al Himdani
fbb4c29d38 Add document CLI commands
Add list, create, view, update, delete, archive, and unarchive
subcommands under `prb document`.

Add document version subcommands: list-versions, view-version,
create-draft, delete-draft, update-version, publish-major, and
publish-minor.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-13 14:22:50 +02:00
Émile Ré
d2937c53d1 Create initial draft version on banner creation
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 15:46:29 +04:00
Émile Ré
61cd3244df Denormalize organization_id onto cookie entities
Add organization_id to cookie_banner_versions, cookie_categories,
and cookie_consent_records so AuthorizationAttributes can return
it directly without JOINing back to cookie_banners. Document the
pattern in contrib/claude/coredata.md.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:52:21 +04:00
Émile Ré
8ba3034828 Add version detection and draft provisioning on publish
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:52:21 +04:00
Émile Ré
88315f51e1 Add cookie banner versioning with JSONB snapshots
Introduce append-only cookie_banner_versions table with a JSONB
snapshot of consent-relevant configuration (privacy policy URL,
consent mode, expiry, categories and their cookies). Each version
has its own state (DRAFT/PUBLISHED) separate from the banner
lifecycle.

Replace the banner state enum (DRAFT/PUBLISHED/DISABLED) with a
simpler ACTIVE/INACTIVE toggle. Link consent records to the
specific published version the visitor accepted.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:52:20 +04:00
Émile Ré
a153427a08 Rename client to service
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:43 +04:00
Émile Ré
c594b10e9f Add consent record operations and rename ConsentRecord to CookieConsentRecord
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:43 +04:00
Émile Ré
327652163f Fix client method names
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:42 +04:00
Émile Ré
15c90b2b72 Fix validation and data access layer
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:42 +04:00
Émile Ré
2cb58d0e32 Fix lint
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:42 +04:00
Émile Ré
992ab642ea Add cookiebanner client
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:42 +04:00
Émile Ré
62c29810a5 Add CookieBannerFilter
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:42 +04:00
Émile Ré
84628683f2 Add migration
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:41 +04:00
Émile Ré
e9330171fe Add new models
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-13 14:50:41 +04:00
Bryan Frimin
43336078f4 Migrate workers to kit/worker
Replace hand-rolled polling loops, semaphores, and WaitGroups
in all 7 background workers with go.gearno.de/kit/worker. Each
worker now implements Handler[T] (Claim/Process) and optionally
StaleRecoverer, gaining automatic Prometheus metrics and
OpenTelemetry tracing. Bumps kit from v0.3.0 to v0.5.0.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-13 12:25:42 +02:00
Sacha Al Himdani
f05f0f9572 Fix missing owner ID in MCP datum responses
The NewDatum type conversion helper was not mapping the OwnerID field
from the coredata struct, causing MCP responses for data resources to
return a zero-value owner_id.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-10 20:47:11 +02:00
Sacha Al Himdani
b84fb1fb4a Remove VERSION order field from MCP document versions
CREATED_AT is sufficient for ordering document versions.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-10 20:15:00 +02:00
Sacha Al Himdani
ff8da296e6 Fix MCP snapshot issues for SOA and vendors
- Fix SnapshotsType enum: STATES_OF_APPLICABILITY → STATEMENTS_OF_APPLICABILITY
- Add snapshot filter to ListStatementsOfApplicability MCP tool
- Add missing organization_id to vendor contacts and compliance reports snapshot inserts

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-10 19:20:00 +02:00
Sacha Al Himdani
eb6c06e683 Move document search_vector from documents to document_versions
Now that title lives on document_versions, the full-text search vector
belongs there too. Drop the generated column and GIN index from
documents, recreate them on document_versions, and update the document
filter to query the latest version's search_vector.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 20:01:32 +02:00
Sacha Al Himdani
221afd8b8d Drop dead description column from documents
The description field on Document and EmployeeDocument was never
populated. Remove it from the database, GraphQL schema, and Go types.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 20:01:32 +02:00
Sacha Al Himdani
b63f043849 Move document title ownership from document to version
Title is now owned by document_versions, following the same pattern as
classification and document_type. The documents.title column is made
nullable with a TODO to drop it. Backend loads title from a
latest_versions CTE for ordering purposes only. The frontend resolves
title from the latest version, and DocumentTitleForm now operates on
DocumentVersion using UpdateDocumentVersion mutation.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 20:01:32 +02:00
Sacha Al Himdani
d519361786 Allow authenticated users to query signUpEnabled
The signUpEnabled field used @session(required: NONE) which rejects
authenticated users. Change it to OPTIONAL so the query works regardless
of authentication state, fixing a crash on the sign-up page for
already-logged-in users.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 19:27:10 +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
Sacha Al Himdani
ab5f42ad74 Redesign document approval flow
Replace the per-approver add/remove model with a quorum-based approval
system. Documents now have default approvers that are pre-populated when
requesting approval, and the publish dialog lets users adjust the list
before submitting.

Key changes:

- Add PENDING_APPROVAL document version status with dedicated transitions
- Introduce approval quorums with request/approve/reject/void lifecycle
- Add default approvers per document (stored in document_default_approvers)
  with MERGE-based upsert for efficient sync
- Add NoDuplicates validator for slice fields
- Split ALTER TYPE ADD VALUE migrations into separate files (required by
  PostgreSQL when run inside transactions)
- Use VOIDED consistently for both quorum status and decision state enums
- Expose void/approve/reject through GraphQL and MCP, with e2e tests
- Add approval management UI: publish dialog with approver selection,
  approval list with void support, and external approve/reject page

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 18:18:30 +02:00
Sacha Al Himdani
5fd580e995 Rename State of Applicability to Statement of Applicability
Rename the entity across the full stack: database table
(states_of_applicability → statements_of_applicability), Go model,
GraphQL types, MCP specification, CLI commands, frontend components,
routes, and display labels. Includes a migration to rename the table
and its foreign key column.

Widen sidebar from 260px to 280px to fit the longer label.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 13:06:06 +02:00