The authorize/Authorize helpers (GraphQL and MCP) already return the
*coredata.Scope resolved from the resource's organization_id attribute,
but several resolvers discarded it and rebuilt the scope with
coredata.NewScopeFromObjectID(...) right after. NewScopeFromObjectID
only reads the tenant encoded in the GID, while the authorizer derives
the scope from loaded resource attributes, so the two silently drift if
the resource lookup ever changes.
Capture scope from authorize and feed it straight to the service/coredata
layer. For the LinkX/UnlinkX MCP tools, move the per-case Authorize
inside the switch and drop the shared scope so each case owns its own
authorization result. Document the rule in contrib/claude/authorization.md
and add a matching .cursor/rules/go-authorize-scope.mdc, including the
narrow exception for global-catalog authorize calls (e.g. identity-scoped
ActionCommonThirdPartyList) where downstream services take no scope.
Signed-off-by: Émile Ré <emile@probo.com>
The trackers list page on the cookie-banner configuration screen needs
to filter rows by the third party that the pattern resolves to. The
tricky part is that "third party" comes from two unrelated tables:
ThirdParty (org-scoped, linked through tracker_patterns.third_party_id)
and CommonThirdParty (global catalog, reached indirectly through
common_tracker_patterns.common_third_party_id). The filter must accept
either flavour of GID and resolve transparently.
Add a single thirdPartyId field to TrackerPatternFilter and dispatch
on the GID's entity-type prefix at the resolver:
ThirdPartyEntityType -> WithThirdPartyID
CommonThirdPartyEntityType -> resolve common_tracker_pattern_id list
via the cookiebanner service, then
WithCommonTrackerPatternIDs
Any other entity type returns an Invalid error rather than silently
matching everything; an unknown caller-supplied GID is a contract bug.
The empty-but-non-nil ID slice produced when a CommonThirdParty has no
patterns yet correctly yields zero rows because the SQL fragment uses
ANY(...).
To populate the filter combobox, add a new CookieBanner.linkedThirdParties
field returning [TrackerPatternThirdPartyLink!]!, a union of ThirdParty
and CommonThirdParty. The resolver collects DISTINCT third_party_id and
common_tracker_pattern_id from the banner's tracker patterns (no joins,
per coredata convention), then chains the catalog lookup through
CommonTrackerPatterns -> CommonThirdParties. Authorization scopes
follow the existing trackerPattern resolvers: ActionTrackerPatternList
gates the aggregation, ActionThirdPartyGet and ActionCommonThirdPartyGet
each gate their respective fan-out only when that branch has work.
The org-scoped fan-out uses dataloadgen.LoadAll so it batches in a
single round-trip; per-key NotFound errors are dropped (a deleted
third party doesn't fail the whole list), other errors bubble up.
Signed-off-by: Émile Ré <emile@probo.com>
Each tracker pattern carries either a direct org-scoped third_party_id
or an indirect link via common_tracker_pattern_id, but the console API
never surfaced either. Expose two optional resolver-driven fields on
the GraphQL TrackerPattern node:
thirdParty: ThirdParty
commonThirdParty: CommonThirdParty
The org-scoped ThirdParty takes priority. When ThirdPartyID is set the
commonThirdParty resolver short-circuits to nil, so the chained
common_tracker_pattern -> common_third_party lookup is only paid for
when a pattern has not been promoted to a tenant-managed third party.
To make the resolver pattern viable across paginated banner trackers
listings, the model now uses @goModel and a custom struct that carries
the foreign-key handles (ThirdPartyID, CommonTrackerPatternID) without
exposing them in the schema. NewTrackerPatternNode populates them from
coredata.
Two new request-scoped dataloaders (CommonTrackerPattern,
CommonThirdParty) batch the chained lookup, mirroring the existing
ThirdParty / CookieCategory loaders. The console mux now wires the
third-party service through dataloader.NewMiddleware so the second
loader has its backing service.
Authorization follows existing precedent: ActionThirdPartyGet for the
org-scoped lookup, ActionCommonThirdPartyGet (granted by the
identity-scoped CommonThirdPartyCatalogPolicy) for the catalog lookup.
ErrResourceNotFound and dataloadgen.ErrNotFound are mapped to a null
field rather than an error.
Signed-off-by: Émile Ré <emile@probo.com>
Surface the third-party links carried by tracker patterns (org-scoped
ThirdParty via third_party_id, or global CommonThirdParty via
common_tracker_pattern_id) requires three new batch loaders and two
filter dimensions, all kept inside their owning entity tables to honour
the no-cross-entity-JOIN rule.
* CommonTrackerPatterns gains LoadByIDs and the ID-only
LoadIDsByCommonThirdPartyID helper, which lets callers translate a
common third party into a set of common_tracker_pattern_id values
without ever JOINing against tracker_patterns.
* CommonThirdParties gains LoadByIDs.
* TrackerPatterns gains LoadDistinctThirdPartyIDsByCookieBannerID and
LoadDistinctCommonTrackerPatternIDsByCookieBannerID, used by the
upcoming CookieBanner.linkedThirdParties resolver to enumerate the
third parties referenced in a banner.
* TrackerPatternFilter gains thirdPartyID and commonTrackerPatternIDs
filter dimensions; the GraphQL layer will dispatch a single
thirdPartyId argument to the right one based on the GID entity-type
prefix.
Service-layer wrappers (cookiebanner.GetCommonTrackerPatternsByIDs,
cookiebanner.LoadCommonTrackerPatternIDsByCommonThirdPartyID,
cookiebanner.LoadDistinctThirdPartyIDsByCookieBannerID,
cookiebanner.LoadDistinctCommonTrackerPatternIDsByCookieBannerID, and
thirdparty.GetCommonThirdPartiesByIDs) expose the new loaders to the
console resolvers and dataloaders that follow.
Signed-off-by: Émile Ré <emile@probo.com>
Use the Connect schema's profile connection in the e2e regression so the test can find the sole owner's membership. Adjust whitespace around the resolver error path to satisfy Go lint.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Return a conflict when membership role updates would demote the final active owner in an organization.
Add an end-to-end regression that verifies the mutation fails and leaves the owner role intact.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Replace the PostHog driver unit test's local HTTP server with the same\nrecorder-backed test style used by the other access-review drivers.\n\nAdd a committed PostHog cassette under testdata so replay mode works\nwithout network access while keeping fixture coverage for role, MFA,\nand timestamp mapping expectations.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Introduce a PostHog access-review driver that lists organization\nmembers and maps role, MFA, and timestamp fields into account\nrecords.\n\nRegister PostHog as a builtin API-key connector provider and expose\nit through the connector provider enum so access-review source\ncreation can discover it.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Align the Metabase driver VCR test with the existing driver fixture
pattern by using a k7 Metabase host in the cassette and default test
instance URL.
This keeps cassette replay deterministic and consistent with the
expected environment naming used by other driver tests.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Implement Metabase as a first-class access review connector backed by
GET /api/user, including account mapping and error handling in the
driver. Register the provider with API-key auth metadata and required
instance URL settings so connectors can be created and resolved
consistently.
Expose Metabase through the console GraphQL and UI flows by adding the
provider enum value, API-key extra setting field wiring, and source
label mapping. Add migration support for the connector_provider enum and
cover driver/provider behavior with focused tests.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Refactor the Grafana driver test to use the shared recorder and VCR
helpers used by other access-review drivers.
This aligns the test with the existing cassette workflow and adds a
committed cassette fixture for deterministic replay.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add Grafana as an access review connector-backed source.
This introduces a Grafana access-review driver, provider registration,
and connector settings for the Grafana base URL. It also wires the
new provider through GraphQL and access-review UI input mapping so
API-key connectors can be created from the product.
A connector_provider enum migration is included so Grafana can be
persisted in existing databases.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
When UpdateUser sets a contract end date that is already in the past,
the user can no longer fulfill outstanding signature requests. Delete
their still-pending requests as part of the same update so they stop
appearing as awaiting signatures.
Signed-off-by: Bryan Frimin <bryan@probo.com>
A signature applies to a whole major: minor publishes keep it and the
export unions signatures across every minor of the major. The request
guard was scoped to a single minor, so re-requesting on a newer minor
(or twice on the same version) inserted duplicate rows and a signatory
appeared several times on the exported signature page.
Deduplicate by loading any existing signature across the major before
inserting, cancel still-pending requests from prior majors when a new
major is published, and restrict the export to active signatories
(comparing contract end dates against the current date). A migration
collapses the duplicate rows already in the table, preferring a signed
row over a pending one and then the most recent.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Wire Cursor into the connector-provider registry as an API-key,
single-tenant connector using HTTP Basic auth, with no picker,
settings, or name resolver. Add the CURSOR enum value, its
migration, and the GraphQL enum binding so the provider is
accepted across the API surface.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Fetch team members from the Cursor Admin API (GET /teams/members)
and map them to access records. The endpoint is not paginated, so
a single request returns the whole team; removed members are
returned as inactive rather than dropped, and team owners are
flagged as admins.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Cursor's Admin API authenticates with the admin key as the HTTP
Basic auth username (empty password) and rejects Bearer tokens.
The API-key connection previously supported only Bearer and a
custom header (Anthropic's x-api-key); add a Basic-auth mode
selected by Registration.APIKeyBasicAuth, and reject providers
that set both it and APIKeyHeader.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Disabling a profile now removes pending document signature requests tied to that signatory. This keeps inactive accounts from retaining signature work across manual, archived, and SCIM deactivation flows.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add the ANTHROPIC connector_provider enum value, its migration, and the
GraphQL enum binding, then register the provider as an API-key connector
that authenticates via x-api-key.
The probe URL is left empty because the shared connection probe cannot
send the required anthropic-version header and would misreport a valid
key; a dead key surfaces on the first member fetch instead.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The driver lists organization members through the Anthropic Admin API
(GET /v1/organizations/users) with cursor pagination, mapping the role
and the RFC 3339 added_at timestamp. The name resolver reads the
organization name from /v1/organizations/me; a non-2xx response (for
example a revoked key) yields no name rather than making the
source-name worker retry forever. Both send the required
anthropic-version header.
Add a VCR test helper that injects the key via x-api-key so the
cassette stays recordable, and strip x-api-key on save. The cassette
holds synthetic members covering the user, developer, and admin roles.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The API-key connection always presented the key as Authorization:
Bearer. Some providers, notably Anthropic, require the key in a custom
header such as x-api-key and reject Bearer auth, returning 400 when
both headers are present.
Add an optional Header field on APIKeyConnection (empty preserves the
Bearer default for every existing provider) served by a small transport
that omits Authorization, plus a registry-declared APIKeyHeader so the
create-connector resolver wires the right scheme per provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
GET /teams/@personal 404s, which would loop the source-name worker the
same way a stale Sentry slug did. Short-circuit the personal-account
slug to a static name before any HTTP call.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The driver now runs in personal mode when no team is configured, so the
team_id-required guard would block solo accounts from ever building a
driver. Drop it and let an empty or personal-account slug flow through.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Heroku Teams are opt-in, so a solo account surfaces no options and the
picker used to dead-end at a free-text slug the user cannot fill. Always
append a synthetic Personal account entry so the picker offers personal
mode instead.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A personal Heroku account has no Team, so GET /teams returns an empty
list and the team-members path has nothing to fetch. Solo accounts
grant access per app, so enumerate the user's personal apps and collect
each app's owner plus its collaborators, deduped by Heroku user ID.
An empty teamID (or the reserved personal-account slug) now selects this
personal mode. Factor the Range/Next-Range pagination shared by the
members, apps, and collaborators endpoints into herokuListAll.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
GraphQL profile ordering rejected EMAIL_ADDRESS because ProfileOrderField
did not expose this enum value in connect and console schemas.
Add EMAIL_ADDRESS to MembershipProfileOrderField and its validation list
so order input coercion accepts the value consistently. Extend
MembershipProfile cursor key encoding to support email ordering and avoid
runtime panics during pagination.
Update the MCP profile order enum to keep API surface definitions aligned
with the same ordering capability.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Allow vendor country selections to use a Global region alongside
existing country and EU values. The new value is accepted by backend
country-code validation, exposed through GraphQL and MCP schemas, and
shown in the shared country picker label set.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
ListAccounts on a connector whose stored slug is no longer accessible
to its OAuth token currently returns "cannot fetch sentry members:
unexpected status 404" -- opaque, and indistinguishable from a real
Sentry outage. The campaign source-fetch worker records that string
verbatim as the customer-visible LastError, with no hint that the
connector itself needs reconnection.
queryMembers now returns a sentinel errSentryOrgNotAccessible on 404,
and ListAccounts wraps it with the slug and a directive to reconnect.
errors.Is preserves the chain so future callers can branch on the
permanent-config-failure case without string matching.
No auto-recovery: the only safe slug is one the customer explicitly
chose. Picking a different visible org would silently rebind the
source to the wrong tenant.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The source-name worker re-claims any AccessSource whose name resolver
returns an error. kit/worker drains tasks in a tight inner loop per
tick, so a permanently-failing resolver hammers Sentry as fast as the
HTTP RTT allows -- in prod, ~5 errors/s for 12h+ on one stale slug.
A 404 from /api/0/organizations/{slug} means the stored slug is no
longer visible to the OAuth token (org renamed/deleted, membership
changed). Retrying cannot recover the name, so return ("", nil) like
the openai and intercom resolvers already do: the worker marks the
row synced, the flood stops, and the source keeps its generic name.
Other non-2xx (401/403/5xx) stay retryable so OAuth refresh and
transient outages still get another chance.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
SentryDriver.resolveOrgSlug duplicated the same /organizations/?member=true
call already implemented in ListSentryOrganizations, which is consumed by
the OAuth org picker. Delegating to the shared helper prevents the two
call sites from drifting (response shape, header set, pagination) and
keeps the driver focused on member listing.
Pure refactor: no behavior change. Add an httptest-backed smoke test
covering the empty-stored-slug path end-to-end through ListAccounts so
the auto-discovery flow stays exercised after the refactor.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Expose viewer.invitingOrganizations: [Organization!]! returning the
organizations that have a live pending invitation directed at the
current identity (accepted_at IS NULL AND expires_at > NOW()). The
list is rendered under a "Pending invitations" section on the
memberships page and in the organization selector dropdown, so a user
already signed in with an existing identity can see which
organizations have invited them without having to dig through their
inbox.
The new field is gated by iam:invitation:list against the viewer's
own identity, so it does not loosen authorization on Organization
elsewhere. E2E coverage validates the live-pending case, the
no-invitation and post-accept cases, and a multi-org scenario.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
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>