Add a dedicated SCIMBridge syncError field to the Connect GraphQL API
and populate it from the core bridge model.
Update the connector cards to read bridge state and syncError directly
from the bridge object instead of inferring failures from event
history.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add a ProfileSource schema to the MCP specification and include source\nin the Profile response schema so MCP clients can distinguish\nmanual and SCIM-managed users.\n\nWire the source field in the Profile mapper so list, get, and\ncreate user responses return the persisted profile source.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Validate ArchiveUser organization input against the loaded profile and\nuse the profile organization for owner checks and webhook emission.\n\nAlso disable both PersonPage destructive actions while either archive\nor remove mutation is pending to prevent double-submit races.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add the required blank line before the pending-invitation filter assignment\nin ArchiveUser to satisfy wsl_v5.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Ensure ArchiveUser deactivates pending invitations for the archived\nprofile so invited accounts cannot still activate after archive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Change ArchiveUser to receive an explicit coredata scope from callers\ninstead of rebuilding scope from organization ID inside the service.\n\nWire connect and MCP resolvers to pass the authorized scope through to\nArchiveUser.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Drop the additional RemoveUser dependency-error handling that was added\noutside this PR scope.\n\nKeep the separate remove and archive operations, but let delete failures\npropagate through existing wrapped errors without the custom helper and\nreferenced-records error type.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Align user-removal UX and API surface semantics with the new archive\nbehavior for manually managed users.\n\nFrontend copy and actions now use archive wording, and list rows are\nrefetched after the mutation so archived users reappear as inactive.\n\nMCP removeUser now documents and returns archived_user_id, n8n labels\nand response mapping now use archive semantics, and the CLI gains a\nuser archive command backed by the same mutation.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Add the required blank line before the profile-state conditional in\nOrganizationService.RemoveUser so the wsl_v5 lint rule passes in CI.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Switch remove-user behavior for manually managed profiles from hard\ndelete to archival by deactivating the profile. This matches the\nrequested SCIM-like lifecycle while avoiding dependency errors for\nlinked records such as signatures and assets.\n\nThe remove flow now updates profile state to INACTIVE, updates\nmembership timestamps, and emits a user-updated webhook event instead of\ndelete events. E2E coverage now asserts that remove keeps the profile\nand marks it inactive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Map remove-user foreign key failures to a dedicated IAM conflict error so\nconsole users no longer receive a generic internal server error when a\nprofile is still referenced by signed documents, tasks, assets, or\nother records.\n\nThe service now detects wrapped dependency errors across both profile\nand membership deletes, and resolvers explicitly map the new error type\nto conflict responses. Added unit tests cover dependency detection and\nthe user-facing error message.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Parse HubSpot archived and deactivated status signals when building
access-review account records. This preserves inactive users as
explicit records instead of dropping them when status fields differ
from the current payload assumptions.
Also accept roleIds payloads and keep records that only expose an
external ID, which is common for archived users with empty email
fields. Add a focused unit test to lock in archived-user handling.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The MCP getSCIMBridge tool authorized on iam:scim-bridge:get, but no
role policy allowed that action, so every call denied — including for
owners and admins.
Owners now get the full iam:scim-bridge:* wildcard (matching the
scim-configuration and scim-event treatment in the same policy).
Admins get read-only access and are explicitly denied create/update/
delete, mirroring how scim-configuration is handled.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Drops the now-unused snapshot_id/source_id columns from every table that
carried them, the snapshots and controls_snapshots tables, the
snapshots_type enum, and the snapshot-scoped indexes. Recreates the
unique indexes that previously gated on snapshot_id IS NULL without the
snapshot scope. The data was already cleaned up in the prior PR.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
CommonThirdParty.AuthorizationAttributes returned an empty map, but
the IAM authorizer requires an entry for every requested resource ID
and treats missing entries as ErrResourceNotFound. With the new
authorize() guard on commonThirdPartyResolver.LogoURL, this surfaced
as "commonThirdParties[*].logoUrl resource not found" from the
CommonThirdPartyComboboxQuery.
Switch AuthorizationAttributes to a SELECT-by-id existence check that
returns an empty attributes map for each row that exists. The catalog
is global with no organization, so omitting organization_id is the
intended shape: the authorizer then loads no membership, evaluates
only identity-scoped policies (CommonThirdPartyCatalogPolicy grants
access), and skips audit logging.
Add an e2e regression test that seeds a CommonThirdParty directly via
the test database and asserts the GraphQL logoUrl path resolves
without error.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
ConnectorProviderInfos listed every known provider, including ones
this deployment cannot connect: no OAuth client credentials configured
and no API-key or client-credentials fallback supported. Those
rendered as dead entries the operator has no way to use, so skip them.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Three follow-ups from review of the registry consolidation:
- Build Vercel's authorization URL with net/url instead of a
hand-rolled "{integration_slug}" placeholder resolved by
strings.ReplaceAll. The slug is escaped via url.PathEscape in a
per-provider Registration.BuildAuthURL closure, and the unused
AuthURLParams plumbing on Registration and OAuth2Connector is
removed (OAuth2Connector now carries a typed IntegrationSlug).
- Drop the SettingsInput union type and the per-provider
MarshalSettings closures. The create resolvers now build the typed
coredata.*ConnectorSettings directly from the gqlgen input, the
same way the OAuth callback path already does, so there is no
shared catch-all DTO and no stringly-typed boundary.
- Restore ConnectorProviders() to a plain ordered slice literal; the
intermediate map + slices.Sort added nondeterminism and a sort for
no benefit.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The console previously dispatched per-provider logic through a fan of
init()-side-effect maps (driver names, OAuth2 metadata, probe URLs,
display names, settings switches), spread across pkg/connector,
pkg/accessreview/drivers and the console v1 resolvers. Adding a new
provider required edits in every one of those places and a corresponding
switch arm in CreateConnectorRequest. The same per-provider knowledge
also leaked into Helm templates as hand-rolled environment-variable
blocks per connector.
This commit collapses the dispatch surface into a single typed
*provider.Registry. The registry is constructed once by
NewBuiltinRegistry at probod startup and threaded as an explicit
dependency into every consumer (accessreview service, console v1
resolver, OAuth2 wiring). There is no package-level state. Each
provider lives in one file under pkg/connector/provider/ that exposes
a private xxxRegistration() *Registration constructor; NewBuiltinRegistry
enumerates them.
CreateConnectorRequest loses its per-provider settings fields and
takes a single RawSettings json.RawMessage produced by the
per-provider MarshalSettings closure. The 1Password SCIM bridge URL
is validated at create time (http(s) scheme + non-empty host) so a
malformed value fails fast at the resolver boundary. The Helm chart
gains probo.connectorEnv and probo.connectorSecretEntries templates
so adding a connector requires zero Helm changes. Access-review name
resolution moves into the same Registration value to keep one
authoritative dispatch table.
Tests cover every Registration (DisplayName, NewDriver wired),
Register error paths (nil, empty Provider, empty DisplayName,
duplicate), All / ProviderDisplayName / ProviderOAuth2Scopes /
ProbeURL hit and miss paths, the ApplyOAuth2Defaults templating and
PKCE branches, and ConnectorSettings[T] round-trip plus malformed-JSON
error path. The pre-refactor ApplyProviderDefaults test in
pkg/connector is replaced by the equivalent in
pkg/connector/provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
CI's staticcheck does not treat t.Fatal as terminating, so every
`err.Message` deref sitting after a separate `if err == nil { t.Fatal(...) }`
block is flagged SA5011 (possible nil dereference). Collapse all 26
occurrences in TestNoHTML / TestPrintableText / etc. into a guarded
`} else if`, matching the fix already applied to validator_format_test.go
in bca36636f. The deref now lives in a branch where err is provably
non-nil, which satisfies SA5011 regardless of t.Fatal recognition.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Replace strings.Split + for-range-slice with strings.SplitSeq +
for-range-iterator in checks_linux.go and checks_windows.go. These
files are invisible to go fix on macOS (darwin build tag), so the CI
Linux runner caught them first.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Validate executable and state directory paths before rendering the
rc.d script so crafted values cannot inject shell syntax.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Treat sc.exe delete error 1060 as success when the service is
already absent so repeated uninstalls do not fail.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Swap the keystore through a temp and .old file on Windows instead
of relying on os.Rename alone, which is not reliably atomic there.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Move the duplicated file-copy logic into update/copy.go so both
platform installers stay in sync during maintenance.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Return unknown when service status commands fail so remote login
and malware checks cannot pass on empty output after an error.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Introduce the standalone device agent binary and shared library
for enrollment, posture checks, self-update, and OS service
integration. Include build targets, module deps, and release
workflow so the agent can ship independently of server changes.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Relative redirect URLs are now normalized before validation. Paths
containing backslashes (including percent-encoded %5c) are rejected,
closing a bypass where /../\evil.com passed checks but http.Redirect
normalized to /\evil.com.
Reported by Fushuling and RacerZ.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Every PromoteSource caller already loaded the tracker pattern under
the same transaction, so a dedicated single-column UPDATE only
duplicated machinery and forced callers to learn a second mutation
verb. Add `source = @source` to Update's SET clause, mutate
Source/UpdatedAt on the receiver, and call Update at the three
promotion sites (worker merge loop, worker adoption loop, and
reportDetectedTracker). The shouldPromoteSource gate still ranks the
candidate against the loaded value; Update is now the single write
path that can advance source, with a doc comment spelling out the
load-first contract.
Re-cast the coredata tests around Update: WritesSource pins the
round-trip from receiver to DB, NotFoundForMissingRow preserves the
ErrResourceNotFound contract callers rely on. The old
OnlyTouchesSourceAndUpdatedAt test was a property of the narrow
PromoteSource UPDATE and no longer applies — Update intentionally
rewrites the full editable column set from the receiver.
Signed-off-by: Émile Ré <emile@probo.com>
The pattern-analysis worker dropped two signals on every run. When
InsertIfNotExists hit a pre-existing glob, the computed bestSource
was discarded by the LoadByBannerIDTypeAndPattern fallback, so the
SCRIPT > EXTENSION > PRE_EXISTING precedence advertised on
bestSource was only ever enforced at first insert. Subsequent
batches with stronger sources could not promote the glob, even
though the page-script-wins rule already lives in detected_trackers
at the row level.
Separately, adoptUncategorisedPatterns returned an adopted bool
that the worker discarded; the function moves detected trackers
from uncategorised exact patterns into categorised globs, which is
a real consent transition, but no draft banner version was created
on adoption-only runs.
Add a focused TrackerPattern.PromoteSource that only updates the
source and updated_at columns. Express the precedence as a pure-Go
shouldPromoteSource helper alongside bestSource so the rule is
unit-testable without a database. The worker now calls
InsertIfNotExists, then on conflict loads, skips when the slot is
held by an exact pattern or a user-recategorised glob, and only
calls PromoteSource when the candidate source ranks above the
existing one.
The skip branch is now documented: adoptUncategorisedPatterns is
the safety net that re-homes uncategorised exacts into the existing
glob via globMatch. Capture its adopted return value and use it
(instead of the previous over-eager consentChanged flag) to gate
ensureDraftVersionForBanner. Merging exacts into a glob in their
own category never changes visitor consent, so the prior flag
produced redundant draft versions on every non-uncategorised merge.
Cover the new pieces with three test layers: pure-unit cases for
shouldPromoteSource (precedence matrix including HTTP/nil collapse
and equal-rank no-write), DB-backed tests for PromoteSource (touch
only source + updated_at, ErrResourceNotFound for missing rows),
and end-to-end worker tests for source promotion on an existing
glob, draft-on-adoption, and the merge-only no-draft case.
Signed-off-by: Émile Ré <emile@probo.com>
Add a many-to-many relationship between measures and third parties,
surfaced as a measures tab on the third party detail page and a third
parties tab on the measure detail page. Each side gets a paginated
list with a link/unlink dialog.
Also remove the right-hand drawer on the measure detail page and
expose the state as a badge in the page header, mirroring how the
compliance page surfaces its active flag.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Introduce a self-referential many-to-many relation table so a
third party can have child third parties. Each relation is
directional (parent to child); both directions can coexist as
independent rows.
Add a first_level boolean on third_parties (default true) with
a filter on the list page that defaults to showing only
first-level third parties.
Frontend adds a "Third Parties" tab on the detail page where
users can link existing third parties or create new ones from
the common third party catalog (created as non-first-level).
The list page gets a First Level/All toggle filter.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The previous cleanup deleted every isExtensionCaller() site, including
the one in cookie/storage detectors that did fire reliably for the
residual case: page-world extensions (MV3 main world, userscripts with
@grant none) whose stack contains a chrome-/moz-/safari-web-extension
frame at the synchronous write. Recover that signal for free by
returning fromExtension from getInitiatorURL (it already walks the
stack and discards extension frames via continue), and have the cookie
and storage detectors report source: "extension" instead of "script"
when the flag is set.
End-to-end plumbing reuses the existing source column: extend the
cookie_source Postgres enum with EXTENSION, add the CookieSourceExtension
constant with a doc block describing each bucket's actual semantics,
add the handler.go switch cases, expose EXTENSION on the GraphQL and
MCP CookieSource enums, and add the Extension option to the console
source filter.
Update bestSource in the pattern analysis worker so a glob merging
only extension-attributed exact patterns is no longer silently rolled
up to PRE_EXISTING. New precedence is SCRIPT > EXTENSION > PRE_EXISTING,
matching the upsert SQL's "page-script wins" rule and the asymmetric
signal strength of each bucket.
Out of scope: any behavioural use of EXTENSION (auto-exclusion,
denylist classification, dashboard surfacing) -- that belongs in the
follow-up backend denylist plan.
Signed-off-by: Émile Ré <emile@probo.com>