Reject deletion while a campaign is fetching sources so workers are
not racing a removed record. All other statuses remain deletable.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Drop the backend status gate on campaign delete and show delete in
the console whenever the user has delete permission, regardless of
whether the campaign is draft, in progress, or completed.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Introduce accessreview.Campaign with draft/deletable predicates and
operation-specific client errors. Drop errUnlessDraftCampaign and
status-to-sentinel switches in the service layer.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Return GraphQL invalid when cancel hits completed or cancelled
campaign status sentinels from the service layer.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
List campaign status sentinels explicitly at resolver AnyOf
call sites instead of a shared slice.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Centralize errors.Is OR chains in access review campaign resolvers
and expose CampaignStatusErrors for the four non-draft status
sentinels.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Replace ErrCampaignCannotStart and ErrCampaignCannotUpdate with
sentinels per campaign status, mapped through CampaignStatusError
and wrapped with operation-specific fmt.Errorf prefixes.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Use ErrCampaignCannotStart and ErrCampaignCannotUpdate with clear
actionable messages instead of a generic draft-status sentinel
wrapped in noisy fmt.Errorf chains.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The failure is about unconfigured campaign sources, not an empty UI
selection.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Drop ErrCampaignSourceOrganizationMismatch. When a source ID is
missing or belongs to another organization, return
coredata.ErrResourceNotFound so clients get a generic not-found
response instead of leaking cross-organization details.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Align the sentinel name with the validation it represents.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Follow the cookiebanner pattern: grouped var Err* sentinels in the
service package, wrapped with fmt.Errorf where context is needed, and
explicit errors.Is checks in GraphQL resolvers.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Introduce sentinel and structured errors for access review campaign
validation failures, and map them to INVALID in GraphQL resolvers via
errors.Is rather than matching error message prefixes.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
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>
Campaigns cannot be started without at least one scope source, but the
n8n node had no way to attach sources after creation. Expose the
addAccessReviewCampaignSource mutation as an Add Source operation so
workflows can configure sources before starting.
Return INVALID instead of INTERNAL when start fails due to missing
sources or invalid status, so n8n surfaces the real error message.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Org-defaulting for picker providers only ran in the GraphQL resolver,
so a picker-provider source created or updated through the MCP API
connected fine but resolved no users until the org was picked. Move
the defaulting into the accessreview service as
AutoSelectDefaultOrganization and call it from both surfaces, moving
the providerOrgConfigs picker dispatch alongside it (the three console
picker resolvers now dispatch through service accessors, behavior
unchanged).
Also harden the moved path: resolve the provider from cheap connector
metadata before building the authenticated HTTP client, so the ~50
non-picker providers no longer pay a decrypt/refresh/DB-write on every
create/update; bound the outbound ListOrgs call with a 10s timeout so
a hung provider cannot stall the mutation; and re-check inside the
ConfigureAccessReviewSource tx (OnlyIfUnset) so an org the user picks
while ListOrgs is in flight is not overwritten by the first listed
org.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Picker providers (GitHub, Sentry, GitLab, Bitbucket, Heroku, Asana,
Netlify, ClickUp, DocuSign) require the user to pick an org/workspace in
a follow-up step after connecting. When that step is skipped, the source
stays connected but unconfigured, and its first campaign silently
resolves no users because the driver needs an org — the same "connects
fine, campaign fetches nobody" symptom seen on Sentry.
When a connector is linked to a source, auto-select the first workspace
the connection can list, so the source is usable immediately. An org the
user already chose is never overridden, and the picker stays visible
(the frontend shows the selector whenever an org is selected) so they
can switch when several are listed.
Best-effort: a provider that is unreachable or lists nothing leaves the
source in its existing needs-configuration state rather than failing the
create/update mutation.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Expose campaign sources as first-class nodes, paginate fetch attempts
instead of denormalized status fields, and bind entries to their
campaign snapshot. Update GraphQL, MCP, CLI, console, and e2e coverage
to match.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Bad API keys and expired OAuth tokens showed Connected because
probes ran only for OAuth2 and many providers had no ProbeURL.
Add a registry ProbeConnection dispatcher with static, dynamic,
and custom probes so all 41 providers are checked on demand.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Decouple each campaign from the live access-review sources it was started
with by introducing a per-campaign source snapshot table
(access_review_campaign_sources). The snapshot captures the source name,
category, and connector at start time, so a review remains coherent even
after the underlying source is edited or deleted. Fetch tracking becomes
an append-only log (access_review_campaign_source_fetch_attempts) that
preserves every attempt with its own status and error rather than
overwriting a single row.
Rename the shared access-review tables and enums to use a consistent
access_review_ prefix throughout:
access_entries → access_review_entries
access_sources → access_review_sources
access_source_category → access_review_source_category
access_entry_* → access_review_entry_*
The same rename propagates to every coredata type, service, GraphQL
schema, MCP specification, CLI command, frontend component, and e2e test.
The accessreview package gains dedicated actions.go and policies.go files
for its own IAM policy set, mirroring the agentrun package pattern.
Signed-off-by: Bryan Frimin <bryan@probo.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>
Audited pkg/server/api/console/v1 for resolvers that touched tenant
data without calling r.authorize, batchAuthorize, or Permission. Closed
every gap so every data-bearing field goes through IAM (and produces an
audit log entry when an organization_id is present).
* High-severity reads now authorize: accessSourceResolver.Connector and
ConnectionStatus, controlResolver.Regulatory/Contractual/RiskAssessment,
electronicSignatureResolver.CertificateFileURL/Events,
commonThirdPartyResolver.LogoURL, and the proper
accessSourceResolver/accessReviewCampaignResolver/auditLogEntryResolver
Organization resolvers (authorize + dataloader load, fixing the latent
empty-name bug from the previous force-resolver no-op implementations).
* TotalCount/DetectedCount aggregates now authorize the matching list
action across access review, audit log, statement of applicability,
detected tracker, tracker pattern, and tracker resource connections.
* queryResolver.CommonThirdParties authorizes against the principal's
identity via the new identity-scoped CommonThirdPartyCatalogPolicy.
* Add ActionCommonThirdPartyGet/List, ActionElectronicSignatureGet probo
action constants; wire ActionElectronicSignatureGet into ViewerPolicy
and AuditorPolicy.
* Implement AuthorizationAttributes on CommonThirdParty (no org) and
ElectronicSignature (organization_id) so the authorizer can resolve
attributes for the new actions.
* Delete the dead "type AccessReview" GraphQL type (no Go constructor,
no frontend reference) and drop its orphan resolver bundle.
Signed-off-by: Bryan Frimin <bryan@probo.com>
- Wire post-OAuth pickers and 2-auto callback handling
- Add 13 vendor logo components for new connectors
- Wire access-review connectors into bootstrap config
- Track e2e gap for new access-review connectors
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The go-sdk v1.6.0 release no longer applies a default
CrossOriginProtection when the field is nil in StreamableHTTPOptions,
silently removing Origin header verification. Wrap the streamable
handler with http.NewCrossOriginProtection().Handler(...) (the
recommended replacement, since the SDK field is deprecated).
Also regenerate gqlgen resolvers to track v0.17.90.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Change gqlgen filename_template from {name}.resolvers.go to
{name}_resolvers.go across all three APIs for consistent Go naming.
Signed-off-by: Émile Ré <emile@getprobo.com>