957 Commits

Author SHA1 Message Date
Sacha Al Himdani
6a4f124adb Replace third-party owners with administrators
Some checks failed
github / Analyze (go) (push) Has been cancelled
github / Analyze (actions) (push) Has been cancelled
github / Analyze (javascript-typescript) (push) Has been cancelled
make / build-apps (push) Has been cancelled
make / probod binary (darwin/amd64) (push) Has been cancelled
make / probod binary (freebsd/amd64) (push) Has been cancelled
make / probod binary (linux/amd64) (push) Has been cancelled
make / probod binary (openbsd/amd64) (push) Has been cancelled
make / probod binary (windows/amd64) (push) Has been cancelled
make / probod binary (darwin/arm64) (push) Has been cancelled
make / probod binary (freebsd/arm64) (push) Has been cancelled
make / probod binary (linux/arm64) (push) Has been cancelled
make / probod binary (openbsd/arm64) (push) Has been cancelled
make / probo-agent (darwin/amd64) (push) Has been cancelled
make / probo-agent (freebsd/amd64) (push) Has been cancelled
make / probo-agent (linux/amd64) (push) Has been cancelled
make / probo-agent (windows/amd64) (push) Has been cancelled
make / probo-agent (darwin/arm64) (push) Has been cancelled
make / probo-agent (freebsd/arm64) (push) Has been cancelled
make / probo-agent (linux/arm64) (push) Has been cancelled
make / probo-agent (windows/arm64) (push) Has been cancelled
make / docker (amd64) (push) Has been cancelled
make / docker (arm64) (push) Has been cancelled
make / snapshot-scan (push) Has been cancelled
make / build-probod (push) Has been cancelled
make / build-probo-agent (push) Has been cancelled
make / lint-go (push) Has been cancelled
make / lint-js (push) Has been cancelled
make / lint-swift (push) Has been cancelled
make / lint-shell (push) Has been cancelled
make / test (push) Has been cancelled
make / test-e2e (push) Has been cancelled
trufflehog / scan (push) Has been cancelled
Migrate business and security owners into a shared administrators list across GraphQL, MCP, CLI, n8n, and the console.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-31 16:48:36 +02:00
Bryan Frimin
36e038f5ff Join identities in MembershipProfiles.LoadExistingByIDs
SCIM event CSV exports failed with "cannot parse address :
mail: no address" because this query selected a placeholder
empty string for email_address instead of the real address, and
scanning it into mail.Addr tried to parse it as one.

Join identities the same way every other MembershipProfile query
already does, and drop the redundant identity lookup in the SCIM
export path now that profiles carry their real email address.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-31 09:39:22 +02:00
Ludovic Vielle
f998a35357 Scope device enrollment token deletes
Delete and DeleteExpired omitted Scoper, breaking the
tenant-isolation pattern used elsewhere in coredata. Pass a
tenant scope from ExchangeEnrollmentToken and NewNoScope from
the ITAM GC so cross-tenant cleanup stays explicit.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-30 17:19:17 +02:00
Cursor Agent
a26a4ad898 Defer weekend document reminders to Monday
Reminders that fall due on Saturday or Sunday no longer send
over the weekend or spend the escalation ladder unused. SQL
keeps calendar cadence and rolls weekend due times to Monday
at the same clock hour. The first debounced notice is unchanged.

Co-authored-by: Sacha Al Himdani <sacha@probo.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-07-30 15:14:19 +00:00
Ludovic Vielle
7bce67454c Map ITAM actions to v1:itam OAuth2 scopes
ITAM GraphQL actions were never registered in the shared OAuth2
scope registry, so bearer-token callers failed closed even when
role policies allowed them. Add v1:itam / v1:itam:read mappings,
register them in probod, and sync the CLI client scopes.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-30 16:34:17 +02:00
Ludovic Vielle
7731566c68 Add soft delete for revoked devices
Admins could only revoke devices, so never-enrolled and revoked
inventory rows piled up with no way to remove them. Soft-delete
is limited to REVOKED devices (revoke first), and ITAM GC now
hard-deletes PENDING/REVOKED orphans with no API key, postures,
or valid enrollment token—including user tombstones without
history.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-30 16:34:16 +02:00
Cursor Agent
d598715b46 Include activated_at in SCIM export profile load
LoadExistingByIDs scanned into MembershipProfile without
activated_at/deactivated_at after the profile-state split, so
SCIM_EVENT CSV exports failed collecting rows by ID.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 12:24:27 +00:00
Cursor Agent
5ea732a97f Resolve SCIM export profiles from event path IDs
SCIM events store user_name, not identity_id; profile GIDs
in /Users/{id} paths drive LoadExistingByIDs and identity
email lookup instead of batching by user_name.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:13:17 +00:00
Cursor Agent
c5b1f210d5 Harden log export CSV and fix profile loader boundary
Load SCIM profile emails via Identities in the export
layer, drop the cross-table join, sanitize formula-leading
cells, and fall back to validated userName when profile email
is missing. Drop redundant gid array casts in LoadByIDs.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
6906e0ff5b Separate SCIM export email from event user name
Load identity email for matched profiles and drop parsing
userName as email; user_name column stays the event value.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
6cbf16814c Use coredata models in log export batch loaders
Drop export-only row types; load Identity, PersonalAPIKey,
and MembershipProfile through the usual slice LoadByIDs helpers.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
e4d86f0f9e Add narrow audit log actor loaders for CSV export
Replace full Identity and PersonalAPIKey batch loads with
rows that select only id, email, name fields used in export.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
7bba43b902 Select only SCIM export profile columns by user name
The batch loader no longer scans into MembershipProfile with
placeholder email and organization name columns from dropped
identity and organization joins.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
ced25bad32 Load SCIM export profiles without identity join
Membership profile rows already carry SCIM userName and
name fields; export email comes from userName, not login
identity email.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
b715107e6c Drop empty-input guards from batch loaders
ANY(@ids) with an empty array is valid in Postgres and
CollectRows already yields an empty slice; the early nil
assignments added noise without changing callers.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
5cd407bd86 Emit audit and SCIM log exports as CSV
JSONL was awkward in spreadsheets and SIEM imports. Write
tab-separated-friendly CSV with organization name on every row,
resolve audit actors to email or API key name, and enrich SCIM rows
with profile email and display name when available.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Émile Ré
eecf8a1d97 Make profile state filters multi-value
Address PR review feedback on the profile-state split: SAML sign-in now
activates a pending profile, deactivation counts owners against the
profile's own organization to close a last-owner bypass, the migration
leaves historical activated_at/deactivated_at NULL rather than
fabricating timestamps, and pending members are no longer rendered with
the deactivated (faded) styling.

Drop the single-value state filter in favor of the multi-value states
across the profile and signatory surfaces. Remove ProfileFilter.state
(only states[] remains) and convert the signatures profileState filter
to profileStates. Turn the console people filter, the CLI
"user list --state" flag, and the n8n listUsers and getAllSignatures
state inputs into multi-select controls, where an empty selection means
all states.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Émile Ré
4a276e3ef7 Split inactive profile state
Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.

Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.

A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Sacha Al Himdani
cd6c46212a Add log export for audit logs and SCIM events
Route audit-log and SCIM-event exports through export_jobs with typed
arguments, an iam BuildAndUploadExport/SendExportEmail implementation,
and a concurrent export-job worker with stale recovery. Stream JSONL via
page.WalkAll into S3, and expose the request flow on console, connect,
MCP, and CLI.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-29 18:57:31 +02:00
Cursor Agent
255bea4738 Make risk delete prove document mapping cleanup
Removing junction rows before deleteRisk hid missing soft-delete
cleanup in e2e. Assert deleteRisk succeeds only after deleteDocument
clears mappings, and document links must be removed first.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:34 +02:00
Cursor Agent
f451e94c0b Clear document mappings on soft delete
Document soft delete cleared generated-document references but left
control, risk, and measure junction rows pointing at deleted documents.
That blocked risk deletion and made unlinkRisk fail when the document
was already gone.

Remove entity mappings in SoftDelete and BulkSoftDelete, drop mappings
before deleting a risk, tolerate missing documents when unlinking, and
backfill orphaned junction rows for soft-deleted documents.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:32 +02:00
Ludovic Vielle
ad615a47a0 Show posture values and report history
Pass/fail was the main device UI signal, but operators need
the agent's observed value. Expose a formatted value per
check, show current postures on the device page, and replace
the Postures tab with paginated report history grouped by
agent push time. Status stays in the model for later rulesets.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-29 16:27:23 +02:00
Cursor Agent
9e9272b4ac Add sort fields for audit engagement dates
Expose AUDIT_START_DATE and AUDIT_END_DATE on AuditOrderField
so list queries can paginate like valid_from and valid_until.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:34 +02:00
Cursor Agent
c7b1562d26 Store audit engagement dates as DATE columns
Match valid_from and valid_until on audits and the console
date-only inputs for audit start and end.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:34 +02:00
Cursor Agent
f8f5152566 Use timestamptz for audit engagement dates
Store audit_start_date and audit_end_date as TIMESTAMP WITH
TIME ZONE. Alphabetize n8n audit field collections to satisfy
n8n-node lint.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:33 +02:00
Cursor Agent
fe215efbd1 Add audit start and end dates to audits
ISO audits often span a window distinct from certificate validity.
Store optional audit_start_date and audit_end_date on the audit
record and expose them through GraphQL, MCP, CLI, n8n, and console.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:33 +02:00
Émile Ré
b64f101ab9 Drop resend verification email cooldown
Resend only runs after an explicit form submit, so a per-address
cooldown is unnecessary overhead compared with forgot-password.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:06 +02:00
Émile Ré
82a62005f8 Harden email verification resend against abuse
Add a per-address confirmation-email cooldown and disable the
resend/forgot-password submit buttons while the mutation is in
flight so callers cannot flood the mail queue or double-submit.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:06 +02:00
Bryan Frimin
636dd172e8 Fix wsl_v5 spacing in MergeByCampaignID
Add blank lines around the dedup-and-connector-ID block to satisfy
the wsl_v5 linter (blank line after an if-block, above an assignment
following a block, and above an if with no shared variables in the
preceding line).

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:25 +02:00
Bryan Frimin
b43e66ffa4 Remove restated doc comment on MergeByCampaignID
The comment repeated what the function signature and body already
say plainly.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:25 +02:00
Bryan Frimin
68a64647bf Batch-load and validate campaign sources before merging
MergeByCampaignID joined access_review_sources directly from coredata
to resolve live sources, so unrecognized or out-of-scope IDs were
silently dropped instead of erroring, and in the worst case (every ID
invalid) the NOT MATCHED BY SOURCE clause deleted every existing
campaign source. The syncCampaignSources ErrResourceNotFound check
was therefore unreachable dead code.

Add AccessReviewSources.LoadByIDs, matching the existing scoped
LoadByIDs pattern (id = ANY(@ids) plus a resolved-count check), and
have CreateCampaign, AddCampaignSource, and syncCampaignSources
resolve and validate sources up front. MergeByCampaignID now takes
the already-loaded sources and builds its desired-state CTE from an
unnest() of their values instead of joining access_review_sources,
keeping the merge inside the campaign-source entity boundary.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:24 +02:00
Émile Ré
15b51640d3 Fix geoloc country ip block import script
Signed-off-by: Émile Ré <emile@probo.com>
2026-07-27 18:21:57 +02:00
Cursor Agent
3455784f52 Remove unused uniqueGIDs helper
MergeByCampaignID already dedupes with gid.NewSet.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
b5574764cf Dedupe campaign source IDs with gid.NewSet
Remove local uniqueGIDs helper in favor of the existing gid set
utility used elsewhere in coredata.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
3b3d7002a9 Drop pre-merge access source count check
Scoped rows in the MERGE desired_sources CTE already limit
snapshots to sources visible under the scoper.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
4d154651a8 Use one path for campaign source merge
Remove early return on empty source IDs; count and MERGE always
run so clearing sources uses NOT MATCHED BY SOURCE delete.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
447bdb88e5 Early-return empty campaign source merge
Delete scoped snapshots in one statement when the desired source
set is empty; validate and merge only when IDs are present.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
abbb1ee875 Merge campaign scope sources in one SQL statement
Add AccessReviewCampaignSources.MergeByCampaignID and use it from
syncCampaignSources instead of per-row load, upsert, and delete.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:36 +00:00
Sacha Al Himdani
d61ec8dd65 Add search and status/role/type filters to People
Makes it practical to find people in larger orgs across GraphQL, MCP, CLI, and n8n, with page size raised to 100.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-27 17:20:18 +02:00
Steven4Hooisma
e62e6cce43 Add UpCloud integration with account listing and details retrieval
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
2026-07-27 11:08:24 +02:00
Aurélien Sibiril
20502fc0be Use the MIT license header on the new connector files
The four connectors landed with the ISC header. contrib/claude/license.md
mandates MIT for every source file, and these were the only ISC files
left in pkg/accessreview/drivers and pkg/connector/provider — the two
"Make license declarations consistently MIT" commits had already cleared
both packages, so this re-opened a closed cleanup.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:05 +02:00
Aurélien Sibiril
60628645ae Add Google Analytics, Dotfile, Segment and Square access-review connectors
Two OAuth2 and two API-key connectors:

- Google Analytics (GA4): OAuth2 with both analytics.readonly and
  analytics.manage.users.readonly (readonly alone 403s on the accounts
  list); v1alpha accessBindings enumerated at account and property level
  and merged by email; manual account picker (Pattern 1) with a
  per-connection probe and name resolver; distinct from Google Workspace.
- Dotfile: API key in the X-DOTFILE-API-KEY header (Pattern 3); GET
  /v1/users (owner/admin, suspended_at) with a static probe.
- Segment (Twilio): Public API token as Bearer with a required Region
  setting (US or EU) mapped to the regional host; GET /users plus per-user
  GET /users/{id} for roles and /invites for pending members; per-connection
  BuildProbeURL.
- Square: OAuth2 (EMPLOYEES_READ) or a personal access token (Pattern 3);
  POST /v2/team-members/search returns email/status/is_owner directly, so no
  role resolution; custom probe and name resolver.

Google Analytics and Square are confidential OAuth clients, wired into the
bootstrap OAuth provider list and .env.example. Segment carries a required
extra setting, so the console add-source dialog maps region onto its
segmentRegion API-key input; without that mapping the value is silently
dropped and the create is rejected.

Cassette-backed driver tests plus unit tests for the Segment probe URL and
the bootstrap OAuth provider list.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:04 +02:00
Ludovic Vielle
1f79453386 Add device data model and ITAM service
Introduce device, posture, and enrollment-token entities with ITAM
service policies for agent-managed fleet inventory.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:21 +02:00
Sacha Al Himdani
bcd05a2e55 Reject empty SAML NameIDs on login
Empty NameID values were stored as '' and occupied the
unique saml_subject index, causing duplicate-key failures
on later logins. Reject blank NameIDs during assertion
validation, return a clear error when a NameID is already
linked to another account, and stop returning internal
errors from the SAML consume endpoint.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-24 14:03:30 +02:00
Bryan Frimin
0b577cda40 Fix global rate limit ACME
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 17:53:20 +02:00
Aurélien Sibiril
a0ead155db Trim access-review connector comments
Cut verbose inline comments across the access-review connector changes:
the mechanical fact stays at the read site, while incident backstory
("millions of error logs in prod") and provider-specific rationale (why
Clerk reviews the wrong population) move to the commit history where they
belong. Also tighten a loose "a 4xx" to "an auth/not-found 4xx" so the
terminal-classification contract is not overstated. No behavior change.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
f711e9d816 Reset access source name sync on connection change
Once the source-name worker sets name_synced_at, nothing cleared it,
so a source that hit a terminal failure (Brex 403, wrong Sentry org
slug) kept its generic name forever — even after the user reconnected
with the right scope or picked the correct org, contradicting the
Brex guidance to reconnect.

Clear name_synced_at whenever the connection changes: on UpdateSource
when a connector is (re)set, inside ConfigureAccessReviewSource when
the org is (re)selected, and on the OAuth reconnect path via the new
ResetSourceNameSyncForConnector service method. The worker then
re-claims the row and re-resolves the display name.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
60d373e5c4 Disable Clerk access-review connector
Clerk's Backend API secret key only exposes the customer's application
end-users (GET /v1/users) -- the people who sign into the customer's
product -- not the Clerk workspace/dashboard team who administer the
platform. Those admins have no Backend API and are managed only in the
Clerk Dashboard UI. So an access-review campaign against Clerk reviews
the wrong population entirely (verified in prod: one source paginating
16K+ end-users), which is a compliance defect, not just the pagination
timeouts it also caused.

Unregister Clerk from the builtin provider registry and drop its driver,
provider registration, tests, and cassette so it can no longer be added
or fetched. Keep the ConnectorProviderClerk enum value, its IsValid
membership, the GraphQL enum value, and the frontend logo so existing
stored CLERK connectors still validate, serialize, and display (users
can still see and remove them). Drop it from ConnectorProviders so the
registry stays in sync with the registered set.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Bryan Frimin
81b7ee5fad Close cert provisioning correctness gaps
Several race and validity gaps could leave certificate provisioning
stuck, unusable, or noisy:

- Accept the HTTP-01 challenge only after the key authorization is
  committed, so the CA cannot hit the token before this instance can
  serve it and invalidate the order.
- Persist challenge metadata under a blocking write-back lock; a row
  merely locked by a competing transaction no longer silently drops the
  accepted order.
- Abandon a recovered VALID order and restart instead of issuing it
  with a freshly generated key that cannot match the existing cert.
- Exclude rate-limited rows from the ten-minute stale reset so the
  resumable order survives the ACME cooldown.
- Size the provisioning poll lease to exceed the max processing window
  so a released claim lock cannot let another worker process the same
  row concurrently.
- Parse Retry-After as unsigned seconds and clamp overflow so malformed
  values fall back to the default cooldown instead of disabling it.
- Normalize the acme_errors problem_type label to the RFC 8555 set to
  bound Prometheus cardinality.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 13:20:53 +02:00
Bryan Frimin
121d4dcf93 Fix certmanager provisioning retry and metrics
Address several provisioning defects that either stalled the retry
budget or crashed the process:

- Classify CAA resolver/transport failures apart from a real CAA policy
  denial. Both shared the "caa records" wording, so a transient resolver
  error was persisted as customer misconfiguration and retried forever
  without consuming the retry budget. A new ErrCAANotPermitted sentinel
  now marks the genuine misconfiguration; other CAA errors are treated
  as ordinary transient failures.

- Honor an explicit Retry-After: 0 (or a past date) as permission for an
  immediate retry instead of promoting it to the one-hour default
  cooldown. acme.RateLimit collapses zero, invalid, and absent headers
  to a zero duration, so the header is now parsed directly to tell an
  explicit zero apart from a missing one.

- Reuse already-registered Prometheus collectors when a second
  ACMEService shares a registerer. The fixed-name collectors were
  MustRegistered, so a duplicate registration panicked the process.

- Persist provisioning failures on a context detached from the process
  tick deadline. A timed-out attempt reached persistFailure with an
  expired context, so the write-back failed and the retry budget never
  advanced, leaving the certificate indefinitely retriable.

- Use pgx.StrictNamedArgs in the certificate FOR UPDATE loaders to match
  the coredata SQL contract.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 13:20:52 +02:00