The worker leaned on a lease plus a heartbeat goroutine and a stale
recovery sweep to reclaim runs from crashed workers. That machinery
raced with long LLM and tool calls and conflated graceful stops with
failures. Remove the lease columns, heartbeat, and stale recovery, and
rely on FOR UPDATE SKIP LOCKED for single-claim plus explicit state
transitions: a graceful suspend returns the run to PENDING and a crash
now leaves it RUNNING for manual recovery.
Treat an approval interruption as a known stop that parks the run in
AWAITING_APPROVAL, and add SubmitApproval to merge human decisions into
the checkpoint and requeue the run to PENDING. The decisions must cover
exactly the pending approvals, since a missing one would resume as an
implicit denial. Expose this through the submitAgentRunApproval
mutation.
Signed-off-by: Bryan Frimin <bryan@probo.com>
The agent-run actions and policies lived in the core probo policy set,
which forced every authorization change for the agent-run domain to
touch unrelated core files. Move the actions and the OWNER/ADMIN and
VIEWER/AUDITOR policies into the agentrun package and have it expose a
PolicySet that probod registers into the authorizer at composition
time, so the rules live alongside the domain logic they govern.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Move agent-run orchestration from the legacy supervisor path into the new
agentrun worker/service package and wire it through coredata, server,
policies, and GraphQL resolvers.
This consolidates run lifecycle handling around lease-aware workers and
aligns API surface with the new agent-run domain model so reviewers can
follow one coherent execution path.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Rejecting one audit report via Slack could look like a blanket denial
when HIPAA and SOC 2 reports shared a filename. Use framework and
audit name in rejection emails.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Zendesk is a multi-tenant OAuth connector keyed by the customer
subdomain. The customer enters it at connect time; it rides the signed
state to the callback, is re-validated, and is stored on the connector
settings to build the API host.
List staff (agents and admins) via GET /api/v2/users.json with cursor
pagination, mapping role, active/suspended, and 2FA status; end-users
are excluded. The subdomain is validated as a single DNS label at every
trust boundary to close the SSRF vector, and the data client keeps the
SSRF-protected transport.
Zendesk OAuth across customer subdomains requires a Zendesk-approved
global OAuth client; the connector goes live once those credentials are
supplied via bootstrap.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Register the Okta provider (SupportsAPIKey, APIKeyAuthScheme SSWS, a
required "domain" extra setting, and the driver/name-resolver
factories) and add it to the builtin registry.
The create-API-key resolver normalizes and validates oktaDomain into
OktaConnectorSettings, returning a static INVALID error that never
echoes operator input, and stamps the SSWS scheme onto the
connection. No picker, OAuth metadata, or probe URL: the token plus
domain identify exactly one org and the host is per-connection.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Introduce the OKTA value to the connector_provider enum (Go const,
ConnectorProviders slice, IsValid switch, GraphQL @goEnum, and the
Postgres ALTER TYPE migration) and an OktaConnectorSettings struct
holding the customer's Okta org domain.
Okta is a per-tenant IdP with no central API gateway, so the
connector is keyed on the org domain rather than a shared host. The
oktaDomain field on CreateAPIKeyConnectorInput lets the API-key flow
carry it.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Wire Clerk in as a supported connector provider for access\nreviews and expose it through the console GraphQL provider enum.\n\nAdd a dedicated Clerk driver that lists users from the Clerk\nBackend API, maps account state and authentication signals into\nAccountRecord fields, and covers the behavior with focused driver\nand provider tests.\n\nInclude a migration that appends CLERK to the connector_provider\nenum so environments can persist Clerk connectors safely.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Implement a SendGrid access-review driver that fetches teammates
from the SendGrid API and maps them into AccountRecord values.
Register SendGrid as a connector provider, expose it through the
connector provider enum, and add a migration that extends the
connector_provider type with SENDGRID.
Cover the new driver with a VCR-backed fixture test and helper
tests for role and response-shape handling to keep parsing robust.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Fold POSTHOG_SELF_HOSTED into POSTHOG: one provider now covers Cloud (OAuth + region-pinned API key) and self-hosted (API key + instance URL), since both already share the driver, name resolver, and PostHogConnectorSettings{BaseURL}. The API-key form picks a deployment (Cloud US/EU or self-hosted URL); the resolver requires exactly one of region/instanceUrl.
Drop the POSTHOG_SELF_HOSTED enum value, registration, migration, and logo mapping. Extract the deployment selector into a dedicated PostHogDeploymentField component. Point the driver tests at us.posthog.com instead of the legacy app.posthog.com host.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The us/eu host strings lived in the driver and were duplicated in the
connector-settings resolver. Expose drivers.PostHogRegionBaseURL as the
single source and call it from the resolver.
Also parse the base URL only inside the absolute-next branch of
resolveNextURL, where it is actually used for the host check.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Marshal the region (us/eu) for PostHog Cloud and the instance URL
for PostHog Self-Hosted from the create-connector input, validating
each, and add the matching GraphQL inputs and the POSTHOG_SELF_HOSTED
enum value.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A public client identifies itself to the provider with a hosted
Client ID Metadata Document. Serve it unauthenticated and outside the
auth group, since the provider fetches it server-to-server, exposing the
deployment-derived client_id and redirect_uri alongside the Probo brand
name, homepage and logo.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
CommonThirdParty.logoUrl and TrustCenterReference.logoUrl were
returning expiring S3 presigned URLs, which break if cached or
shared past their TTL.
Replace with stable /api/files/v1/{id} application URLs.
file.Service now generates these via baseurl; a new filesign
package owns presigning for the files/v1 HTTP handler that
does the internal redirect.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Expose the generated cookie policy Document on the CookieBanner
GraphQL type through a nullable policyDocument field and resolver,
and surface a link to it from the banner configuration header next
to the origin and ID. The link is hidden until a banner version is
published and the document exists.
Rework the tracker table rows: drop the Source column in favour of
a tracker Type badge, and move each tracker's description inline
beneath its name (and into the add/edit row inputs) instead of a
separate Description column.
Signed-off-by: Émile Ré <emile@probo.com>
Trackers sharing a display name can differ in type, so the generated
cookie and tracking technologies policy was ambiguous without it. Carry
the tracker type through the banner version snapshot and surface it as a
dedicated column in the policy table.
Stop the snapshot from dropping non-cookie trackers so storage, IndexedDB
and cache technologies appear in the policy and served banner config with
their real type. Duration now reflects the type when no max-age applies:
session storage clears with the tab, the remaining storage technologies
persist. Legacy snapshots predate the field and only ever held cookies,
so GetSnapshot backfills an empty type as COOKIE, keeping the non-null
GraphQL enum and policy output valid without a migration.
Signed-off-by: Émile Ré <emile@probo.com>
Queue vetting on third_parties with PENDING, PROCESSING,
COMPLETED, and FAILED states. Expose enqueue and status through
GraphQL, MCP, CLI, and n8n, validate vet requests, tune the
worker via config, and poll the detail page while vetting runs.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The coredata CookieSource enum and the ingestion path both support an
HTTP source, but the GraphQL CookieSource enum never declared it. The
generated marshaler is a plain map lookup with no fallback, so an HTTP
value missed the map and serialized to an empty string. The console UI
treats that empty string as falsy and rendered no source badge at all,
making HTTP-sourced trackers look sourceless.
Add the HTTP member to the GraphQL enum so the value round-trips, and
fold the duplicated tracker-type and tracker-source badge helpers from
three components into a shared @probo/helpers module, adding an explicit
HTTP label while consolidating.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the two-step ID-materializing pattern (fetch IDs in Go, pass
as ANY(@ids)) with an IN-subquery that keeps the filtering entirely
in the database and eliminates an extra round trip. Remove the now
unused LoadIDsByCommonThirdPartyID and its service wrapper. Update
the coredata rule to clarify that subqueries for filtering are OK.
Signed-off-by: Émile Ré <emile@probo.com>
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>
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>
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>
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>
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>
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>
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 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>
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>
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>
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>
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>
The Node resolver was manually constructing a scope from the object
ID, both at the top of the function and again inside several closures.
Since r.authorize already returns the resolved scope, pass it into the
loadNode closures and drop the duplicate construction.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Resolving a typical Console GraphQL query triggers many parallel
authorize calls (one per resource per field resolver). This commit
collapses them via a dataloader: parallel calls within the same
request are gathered into a single iam.Authorizer.AuthorizeMulti pass,
and only fall back to per-item Authorize when AuthorizeMulti rejects
the whole batch (e.g. mixed organizations).
The loader key encodes resource id, action, options, and a canonical
JSON-encoded attribute map so logically identical calls share a key
while differing ones do not. The loader is created without caching so
repeated calls within a request still produce one audit log entry per
call. dataloader.NewAuthorizeFunc preserves the existing
authz.AuthorizeFunc signature and error mapping.
Signed-off-by: Bryan Frimin <bryan@probo.com>