Skip to content

CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens - #165

Open
JavierCarnelli-ConductorOne wants to merge 6 commits into
mainfrom
fix/cxp-1250
Open

JavierCarnelli-ConductorOne wants to merge 6 commits into
mainfrom
fix/cxp-1250

Conversation

@JavierCarnelli-ConductorOne

@JavierCarnelli-ConductorOne JavierCarnelli-ConductorOne commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes CXP-1250.

Summary

RSA public key secrets reported RSA_PUBLIC_KEY_LAST_SET_TIME as their last-used date, so C1 showed when a key was set, not when it was used. Programmatic access tokens (PATs) had no last-used date at all. Either way, C1 could not tell which credentials were actually unused.

This PR reads the real last-use from SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS and attaches it to both secret types.

Changes

Reading the credentials view (pkg/snowflake/credentials.go)

  • New Client.UserCredentials runs one query per sync for KEYPAIR and PAT rows that have a LAST_USED_ON.
  • Results are streamed partition by partition and cached in the session store, grouped by user, so the view is read once per sync rather than once per user and is never fully held in memory.

RSA keys (pkg/connector/rsa.go)

  • DESCRIBE USER now also reads RSA_PUBLIC_KEY_FP / RSA_PUBLIC_KEY_2_FP. Each key is matched to its credentials row by normalized fingerprint (the SHA256: prefix is dropped).
  • LastUsedAt comes from the matched row's LAST_USED_ON. The last-set time is now reported as the resource's created-at.
  • A key that has a fingerprint but no last-set time is now synced instead of skipped.

Programmatic access tokens (pkg/connector/programmatic_access_tokens.go)

  • Tokens are matched to credentials rows by name, and LastUsedAt is set from LAST_USED_ON.
  • created_on from SHOW USER PROGRAMMATIC ACCESS TOKENS is now reported as the resource's created-at.
  • Tokens returned by Issue carry neither date.

When the dates can't be read, the sync still succeeds

  • If the role can't read the view (003001, or 002003 when it has no access to the SNOWFLAKE database), or there is no usable warehouse to run the query (000606), the result is cached as unavailable for the sync. One warning is logged with the matching fix, and RSA keys and PATs are synced without a last-used date.
  • Every other error is returned and fails List, as before. No resources are dropped.

Docs (README.md, docs/connector.mdx)

  • The optional SECURITY_VIEWER grant needed for last-used dates.
  • The query runs on the service account's default warehouse. The setup steps now include GRANT USAGE ON WAREHOUSE.
  • Snowflake records a use up to 2 hours late, and the RSA last-set time now shows as the creation date.

Behavior changes

  • New warehouse use: this is the first required sync statement that needs a warehouse. The SHOW/DESCRIBE calls don't. Each sync may resume the default warehouse once, which Snowflake bills at a minimum of 60 seconds.
  • RSA LastUsedAt changes meaning: it now means actual use. Keys that have never been used, or whose use isn't visible, have no last-used date instead of their set time.

Notes for reviewers

  • The credentials query has no DELETED_ON filter on purpose. ACCOUNT_USAGE.CREDENTIALS has no such column, and Snowflake documents that the view "does not include credentials that have been deleted" (docs). Adding the filter would make the query fail.

Testing

  • Unit tests run against a mocked SQL API. They cover row parsing and NULLs, the async follow-up fetch, a user's rows spanning partitions, loading once per session, unavailable handling for 003001/002003/000606 (cached, one warning per sync), other errors failing and not being cached, and fingerprint and name matching for RSA keys and PATs.
  • go test ./... and golangci-lint (v2.15.0) pass.

🤖 Generated with Claude Code

RSA keys reported the time the key was set as their last-used date, which
made actively used keys look unused. The set time is now the key's creation
date, and last-used dates for RSA keys and programmatic access tokens come
from SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS, read once per sync and cached in
the session store.

Without the SECURITY_VIEWER database role the sync still succeeds, emits
keys and tokens without a last-used date, and logs one warning per sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows without LAST_USED_ON carry no date to report, so filtering them out
reduces the rows, partitions and cache writes per sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 30, 2026

Copy link
Copy Markdown

CXP-1250

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit b5703850fc53

Connector PR Review: CXP-1250

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: full
View review run

Review Summary

The PR adds Client.UserCredentials, which reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync. It streams the results by partition into the session store, grouped by user. RSA keys are matched by normalized fingerprint and PATs by name to fill in a real LastUsedAt. The RSA "last set time" was previously reported as last-used; it now becomes the resource's created-at, and PAT created_on is now emitted too.

I scanned the full PR diff for security and correctness. I also applied the repo-local criteria:

  • S1/S5/R7: satisfied. Only an access-control denial (003001/002003) leads to "no last-used date". Every other error is returned and fails List, and no resources are dropped.
  • L1/L7: satisfied. The denial warning is cached per session, so it logs once per sync.
  • E1/E2: PermissionDenied wrapping is correct.
  • B-series: resource IDs, types and entitlements are unchanged.
  • D-series: docs/connector.mdx is updated.
  • T/J/PR sections: none apply; there are no spans, no new unmarshalers and no Grant/Revoke changes.

There were no prior findings, and nothing in the diff was truncated or dropped.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence), pkg/snowflake/credentials.go:22-23: the query has no DELETED_ON IS NULL filter. A dropped PAT, or a dropped and re-created user, can share a name with a live credential, and findCredential takes the first match, so the deleted row's last-used date can land on the new token.
  • New (medium confidence), pkg/connector/rsa.go:120-123 and pkg/connector/programmatic_access_tokens.go:53-56: this is the first required sync path that runs a SELECT, so it needs a warehouse. A missing or unusable default warehouse returns a 422 that isn't classified as a denial, so RSA/PAT List fails the sync just to get an optional enrichment. That contradicts the new docs ("the sync still succeeds"). Either handle it like the denial path or document the requirement.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 22-23: credentialsQuery reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS without filtering dropped rows. Add `AND DELETED_ON IS NULL` to the WHERE clause, so a deleted PAT or key belonging to a re-created user or token with the same name cannot supply LAST_USED_ON to the live credential. Update the test mocks if they assert on the statement text.

In `pkg/connector/rsa.go` (and `pkg/connector/programmatic_access_tokens.go` around line 53-56):
- Around line 120-123: UserCredentials runs a SELECT, which needs a usable warehouse, unlike the existing SHOW/DESCRIBE calls. If Snowflake returns a "no active warehouse" 422 (e.g. code 000606), the error is not classified as insufficient privileges and List fails the whole sync just to get an optional last-used date. Either extend classifyCredentialsError in pkg/snowflake/credentials.go to treat this case as unavailable (warn once and cache the unavailable status), or document in README.md and docs/connector.mdx that the connector role now needs USAGE on a default warehouse when secrets/PAT sync is enabled.

Comment thread pkg/snowflake/credentials.go Resolved
Comment thread pkg/connector/rsa.go Resolved
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit fa9ea62e6fe5

Connector PR Review: CXP-1250

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: full
View review run

Review Summary

The PR adds Client.UserCredentials, which reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync. It streams the results by partition into the session store, grouped by user. RSA keys are matched by normalized fingerprint and PATs by name to fill in a real LastUsedAt. The RSA "last set time" was previously reported as last-used; it now becomes the resource's created-at, and PAT created_on is now emitted too.

I scanned the full PR diff for security and correctness. I also applied the repo-local criteria:

  • S1/S5/R7: satisfied. Only an access-control denial (003001/002003) leads to "no last-used date". Every other error is returned and fails List, and no resources are dropped.
  • L1/L7: satisfied. The denial warning is cached per session, so it logs once per sync.
  • E1/E2: PermissionDenied wrapping is correct.
  • B-series: resource IDs, types and entitlements are unchanged.
  • D-series: docs/connector.mdx is updated.
  • T/J/PR sections: none apply; there are no spans, no new unmarshalers and no Grant/Revoke changes.

There were no prior findings, and nothing in the diff was truncated or dropped.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence), pkg/snowflake/credentials.go:22-23: the query has no DELETED_ON IS NULL filter. A dropped PAT, or a dropped and re-created user, can share a name with a live credential, and findCredential takes the first match, so the deleted row's last-used date can land on the new token.
  • New (medium confidence), pkg/connector/rsa.go:120-123 and pkg/connector/programmatic_access_tokens.go:53-56: this is the first required sync path that runs a SELECT, so it needs a warehouse. A missing or unusable default warehouse returns a 422 that isn't classified as a denial, so RSA/PAT List fails the sync just to get an optional enrichment. That contradicts the new docs ("the sync still succeeds"). Either handle it like the denial path or document the requirement.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 22-23: credentialsQuery reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS without filtering dropped rows. Add `AND DELETED_ON IS NULL` to the WHERE clause, so a deleted PAT or key belonging to a re-created user or token with the same name cannot supply LAST_USED_ON to the live credential. Update the test mocks if they assert on the statement text.

In `pkg/connector/rsa.go` (and `pkg/connector/programmatic_access_tokens.go` around line 53-56):
- Around line 120-123: UserCredentials runs a SELECT, which needs a usable warehouse, unlike the existing SHOW/DESCRIBE calls. If Snowflake returns a "no active warehouse" 422 (e.g. code 000606), the error is not classified as insufficient privileges and List fails the whole sync just to get an optional last-used date. Either extend classifyCredentialsError in pkg/snowflake/credentials.go to treat this case as unavailable (warn once and cache the unavailable status), or document in README.md and docs/connector.mdx that the connector role now needs USAGE on a default warehouse when secrets/PAT sync is enabled.

Reviewed commit: b5703850fc53

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

The credentials query is the first required sync statement that needs a
warehouse. Treat Snowflake's 000606 (no usable warehouse) like the
privilege denial: warn once per sync and emit RSA keys and tokens
without a last-used date instead of failing List. Document the USAGE
grant on the default warehouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JavierCarnelli-ConductorOne JavierCarnelli-ConductorOne changed the title CXP-1250 CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit fa9ea62e6fe5

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since b570385
View review run

Review Summary

The PR reads LAST_USED_ON from SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync, caches it in the session store, and attaches it to RSA keys (matched by fingerprint) and PATs (matched by name). The set time and created_on become the resource's created-at. The new commits add Snowflake error 000606 (no usable warehouse) to the unavailable path (pkg/snowflake/credentials.go:249-255, :104-106), which is cached once per sync with one Warn. Tests now cover 000606, and README.md and docs/connector.mdx include GRANT USAGE ON WAREHOUSE. The full PR diff was scanned for security and correctness.

How the repo-local criteria applied:

  • S1/S5/R7: satisfied. Only an access denial (003001/002003) or a missing warehouse (000606) leads to "no last-used date". Every other error still fails List, and no resources are dropped.
  • L1 (log levels): the warnings are actionable, one-shot Warns, so no sampling is needed.
  • E2 (gRPC codes): FailedPrecondition for a missing warehouse is reasonable and does not touch the retry path.
  • D-series: docs are updated.

There are no go.mod changes and no provisioning changes, so those sections did not apply.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence), pkg/snowflake/credentials.go:132-142: the statement result is fetched from the handle only once. If that GET also returns 202 because a slow ACCOUNT_USAGE query or a cold warehouse is still running, the empty response is parsed as zero rows and cached as loaded. Every RSA key and PAT then syncs without a last-used date, and no warning is logged.

Resolved prior findings

These were earlier summary items with no inline threads.

  • Missing-warehouse 422 failing the sync: fixed. pkg/snowflake/credentials.go:249-255 classifies 000606 as errNoWarehouse, and :60 and :104-107 handle it as unavailable.
  • Missing DELETED_ON IS NULL filter: obsolete. Snowflake documents that ACCOUNT_USAGE.CREDENTIALS excludes deleted credentials and has no DELETED_ON column, so the filter would make the query fail. The query at credentials.go:24-25 is correct as written.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 132-142: listCredentials issues a single GET on the statement handle after a 202. If that GET also returns 202 (the query is still running, which is likely for a warehouse-backed ACCOUNT_USAGE SELECT or a cold warehouse), the response has no resultSetMetaData or data. The function returns nil, and loadCredentials caches "loaded" with no rows. Either poll GetStatementResponse until it returns 200 (bounded by ctx), or check response.resultSetInline() after the GET and, if it is false, return an error or cache "unavailable" with a warning. Add a test where the GET returns 202.

Comment on lines +132 to +142

if c.needsStatementResultFetch(ctx, resp, &response, "ListCredentials") {
req, err = c.GetStatementResponse(ctx, response.StatementHandle)
if err != nil {
return err
}
resp2, err := c.Do(req, uhttp.WithJSONResponse(&response), uhttp.WithErrorResponse(&apiErr))
defer closeResponseBody(resp2)
if err != nil {
return classifyCredentialsError(resp2, &apiErr, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion (medium confidence): this is the first query here that runs on a warehouse against ACCOUNT_USAGE, so it can easily outlast the synchronous window, especially when the warehouse has to resume first. If the single follow-up GET also returns 202 (still running), response has no row-type metadata, no data and no partitions. listCredentials then returns nil and loadCredentials caches loaded with no rows. Every RSA key and PAT then syncs without a last-used date for the whole sync, and no warning is logged. Consider polling the handle until it returns 200, or checking resultSetInline() after the GET and returning an error or caching unavailable with a warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted risk, documented on listCredentials in adc406a. The view is small, so this needs an unusually slow warehouse, and the last-used dates are optional.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a debug log when resultSetInline() is false after the follow-up GET? I think this may happen more often than we're assuming.

Per the SQL API docs, the POST returns 202 once the statement runs past 45 seconds. A GET on a statement that's still running also returns 202 right away instead of waiting. We don't poll or set a timeout, so any run over ~45s almost certainly loses the dates. That includes time spent waiting for a shared default warehouse or for a suspended one to resume, not only a slow query. When it happens, the sync caches loaded with no rows and logs nothing, which looks the same as "nothing was ever used".

A log line with the statement handle would at least show us how often this happens in real tenants, so we can decide later whether polling is worth it.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit adc406a315b0

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since b570385
View review run

Review Summary

The PR reads LAST_USED_ON from SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync, caches it in the session store, and attaches it to RSA keys (matched by fingerprint) and PATs (matched by name). The set time and created_on become the resource's created-at. The new commits add Snowflake error 000606 (no usable warehouse) to the unavailable path (pkg/snowflake/credentials.go:249-255, :104-106), which is cached once per sync with one Warn. Tests now cover 000606, and README.md and docs/connector.mdx include GRANT USAGE ON WAREHOUSE. The full PR diff was scanned for security and correctness.

How the repo-local criteria applied:

  • S1/S5/R7: satisfied. Only an access denial (003001/002003) or a missing warehouse (000606) leads to "no last-used date". Every other error still fails List, and no resources are dropped.
  • L1 (log levels): the warnings are actionable, one-shot Warns, so no sampling is needed.
  • E2 (gRPC codes): FailedPrecondition for a missing warehouse is reasonable and does not touch the retry path.
  • D-series: docs are updated.

There are no go.mod changes and no provisioning changes, so those sections did not apply.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence), pkg/snowflake/credentials.go:132-142: the statement result is fetched from the handle only once. If that GET also returns 202 because a slow ACCOUNT_USAGE query or a cold warehouse is still running, the empty response is parsed as zero rows and cached as loaded. Every RSA key and PAT then syncs without a last-used date, and no warning is logged.

Resolved prior findings

These were earlier summary items with no inline threads.

  • Missing-warehouse 422 failing the sync: fixed. pkg/snowflake/credentials.go:249-255 classifies 000606 as errNoWarehouse, and :60 and :104-107 handle it as unavailable.
  • Missing DELETED_ON IS NULL filter: obsolete. Snowflake documents that ACCOUNT_USAGE.CREDENTIALS excludes deleted credentials and has no DELETED_ON column, so the filter would make the query fail. The query at credentials.go:24-25 is correct as written.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 132-142: listCredentials issues a single GET on the statement handle after a 202. If that GET also returns 202 (the query is still running, which is likely for a warehouse-backed ACCOUNT_USAGE SELECT or a cold warehouse), the response has no resultSetMetaData or data. The function returns nil, and loadCredentials caches "loaded" with no rows. Either poll GetStatementResponse until it returns 200 (bounded by ctx), or check response.resultSetInline() after the GET and, if it is false, return an error or cache "unavailable" with a warning. Add a test where the GET returns 202.

Reviewed commit: fa9ea62e6fe5

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit adc406a315b0

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since fa9ea62
View review run

Review Summary

The PR adds Client.UserCredentials. It reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync, cached per user in the session store, and uses it to set LastUsedAt on RSA keys (matched by normalized fingerprint) and on PATs (matched by name). The RSA last-set time and the PAT created_on are now reported as the resource's created-at.

The new commit (adc406a) changes no code. It only adds a doc comment on listCredentials that records the single follow-up GET as an accepted risk.

I re-scanned the full PR diff for security and correctness. The incremental artifact was complete: nothing dropped and nothing truncated.

The repo-local criteria were applied as follows:

  • S1/S5/R7: satisfied. Only 003001/002003 (no access) or 000606 (no warehouse) produce "no last-used date". All other errors fail List, and no resources are dropped.
  • L1/L7: the warnings are one-shot and actionable, so no sampling is needed.
  • E2: FailedPrecondition for a missing warehouse is reasonable.
  • J3: the nullable times are pointers.
  • Not applicable: sections B10 and PR1–PR3. This PR does not touch provisioning and has no API endpoint spec.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (medium confidence, now acknowledged), pkg/snowflake/credentials.go:137-147: the statement result is fetched with a single follow-up GET and no polling. If Snowflake answers that GET with 202 because the statement is still running, the response is parsed as zero rows and cached as loaded. Every RSA key and PAT then syncs without a last-used date, and no warning is logged. The new doc comment at :120-122 documents this as an accepted risk, and the dates are optional. I'm keeping it as a non-blocking suggestion. Cheap mitigation: if the follow-up response is still pending, log a one-shot Warn (or cache the status as unavailable), so the outcome is no longer silent.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 137-147: listCredentials fetches the async statement result with only one
  follow-up GET (GetStatementResponse). If Snowflake returns 202 again because the statement
  is still running, the empty response is treated as zero rows and loadCredentials caches the
  status as "loaded", so all last-used dates are silently missing. Detect a still-pending
  response after the follow-up GET: either poll with a bounded number of retries and backoff,
  or return a sentinel error that loadCredentials treats like errNoWarehouse (cache the status
  as "unavailable" and log one Warn). That way the degraded outcome is visible and not cached
  as a successful load.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 7995db933eee

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since fa9ea62
View review run

Review Summary

The PR adds Client.UserCredentials. It reads SNOWFLAKE.ACCOUNT_USAGE.CREDENTIALS once per sync, cached per user in the session store, and uses it to set LastUsedAt on RSA keys (matched by normalized fingerprint) and on PATs (matched by name). The RSA last-set time and the PAT created_on are now reported as the resource's created-at.

The new commit (adc406a) changes no code. It only adds a doc comment on listCredentials that records the single follow-up GET as an accepted risk.

I re-scanned the full PR diff for security and correctness. The incremental artifact was complete: nothing dropped and nothing truncated.

The repo-local criteria were applied as follows:

  • S1/S5/R7: satisfied. Only 003001/002003 (no access) or 000606 (no warehouse) produce "no last-used date". All other errors fail List, and no resources are dropped.
  • L1/L7: the warnings are one-shot and actionable, so no sampling is needed.
  • E2: FailedPrecondition for a missing warehouse is reasonable.
  • J3: the nullable times are pointers.
  • Not applicable: sections B10 and PR1–PR3. This PR does not touch provisioning and has no API endpoint spec.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (medium confidence, now acknowledged), pkg/snowflake/credentials.go:137-147: the statement result is fetched with a single follow-up GET and no polling. If Snowflake answers that GET with 202 because the statement is still running, the response is parsed as zero rows and cached as loaded. Every RSA key and PAT then syncs without a last-used date, and no warning is logged. The new doc comment at :120-122 documents this as an accepted risk, and the dates are optional. I'm keeping it as a non-blocking suggestion. Cheap mitigation: if the follow-up response is still pending, log a one-shot Warn (or cache the status as unavailable), so the outcome is no longer silent.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 137-147: listCredentials fetches the async statement result with only one
  follow-up GET (GetStatementResponse). If Snowflake returns 202 again because the statement
  is still running, the empty response is treated as zero rows and loadCredentials caches the
  status as "loaded", so all last-used dates are silently missing. Detect a still-pending
  response after the follow-up GET: either poll with a bounded number of retries and backoff,
  or return a sentinel error that loadCredentials treats like errNoWarehouse (cache the status
  as "unavailable" and log one Warn). That way the degraded outcome is visible and not cached
  as a successful load.

Reviewed commit: adc406a315b0

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

Comment thread pkg/snowflake/credentials.go Resolved
Comment thread pkg/snowflake/credentials.go Resolved
Comment thread pkg/connector/rsa.go Resolved
Comment thread pkg/snowflake/credentials.go Resolved
Comment thread pkg/snowflake/credentials.go Resolved
Comment thread docs/connector.mdx
GRANT DATABASE ROLE SNOWFLAKE.SECURITY_VIEWER TO ROLE <connector-role>;
```

This role is read-only, but it also lets the connector's role read other security information in Snowflake, such as login history. Without it, syncs still work, but these credentials have no last-used date, so C1 never flags them as unused. Reading these dates runs a query on the service account's default warehouse, so the connector's role also needs `USAGE` on that warehouse, with the same fallback if it doesn't. Snowflake can take up to 2 hours to record a credential's latest use. For RSA keys, the date the key was set in Snowflake appears as its creation date.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Docs] The "C1 never flags them as unused" claim isn't backed by the connector

The connector only leaves LastUsedAt unset. What C1 does with a secret that has no last-used date is decided on the platform side. Describe what the connector does instead:

Without it, syncs still work, but these credentials are synced without a last-used date.

RSA List now returns early for users with no key in either slot, so they
no longer cost session store reads, and a tenant without RSA keys or
token sync no longer runs the warehouse query.

Without a session store, UserCredentials now skips the optional dates and
logs one warning instead of scanning the whole view on every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- UserCredentials drops its unused bool result, and its doc now names the
  statement and the privileges it needs.
- Errors from the credentials client no longer repeat the connector
  prefix that the builders already add.
- fetchRemainingPartitions reuses fetchPartitionRows instead of
  duplicating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 7995db933eee

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since adc406a
View review run

Review Summary

The new commit (7995db9) tidies up the credentials client:

  • UserCredentials no longer returns the available flag.
  • With a nil session store, it now skips the ACCOUNT_USAGE.CREDENTIALS query and logs a single process-wide warning. Before, it ran the query for every user. The SDK always passes a WithSyncId-wrapped store (vendor/.../connectorbuilder/resource_syncer.go:158), so this only affects tests and direct callers.
  • fetchPartitionRows moved into client.go and fetchRemainingPartitions now reuses it.
  • RSA List returns early for users with no key fingerprint or set time (pkg/connector/rsa.go:120-123). This matches what rsaResource would emit anyway, because fingerprints are already null-normalized at pkg/snowflake/secrets.go:137-138.
  • The inner error messages dropped the duplicated baton-snowflake: prefix. The callers in rsa.go / programmatic_access_tokens.go still add it.

I scanned the full PR diff for security and correctness. Repo-local criteria were applied:

  • S1/S5/R7: still satisfied. Only 003001/002003/000606 make the last-used dates unavailable; every other error fails List. The new early return emits nothing only for users that have no keys.
  • L7: both warnings are one-shot (cached status / sync.Once).
  • E2: codes unchanged.
  • J/T/provisioning: not affected.

The tests now pass a real in-memory session store and cover the nil-session skip and the user-without-keys skip. The incremental diff was complete (not partial).

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (medium confidence, accepted risk documented at pkg/snowflake/credentials.go:119-121), pkg/snowflake/credentials.go:136-146:
    • The statement result is fetched with one follow-up GET and is never polled.
    • If that GET still returns a pending/202 response, it parses as zero rows and the status is cached as loaded.
    • Every RSA key and PAT then syncs without a last-used date, and no warning is logged.
    • Cheap mitigation: detect the still-pending response and either log a one-shot Warn or cache the status as unavailable.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 136-146: After the single follow-up GET for the statement result, if Snowflake still reports the statement as running (HTTP 202 / no resultSetMetadata), do not treat it as an empty successful result. Either return a sentinel (e.g. errStatementPending) that loadCredentials maps to credentialsStatusUnavailable with a one-shot Warn ("secret last-used dates unavailable; credentials query did not finish in time"), or poll the handle a bounded number of times with ctx-aware backoff before giving up.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: CXP-1250 fix: report real last-used dates for RSA keys and programmatic access tokens

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base eef5d354c285.
Review mode: incremental since adc406a
View review run

Review Summary

The new commit (7995db9) tidies up the credentials client:

  • UserCredentials no longer returns the available flag.
  • With a nil session store, it now skips the ACCOUNT_USAGE.CREDENTIALS query and logs a single process-wide warning. Before, it ran the query for every user. The SDK always passes a WithSyncId-wrapped store (vendor/.../connectorbuilder/resource_syncer.go:158), so this only affects tests and direct callers.
  • fetchPartitionRows moved into client.go and fetchRemainingPartitions now reuses it.
  • RSA List returns early for users with no key fingerprint or set time (pkg/connector/rsa.go:120-123). This matches what rsaResource would emit anyway, because fingerprints are already null-normalized at pkg/snowflake/secrets.go:137-138.
  • The inner error messages dropped the duplicated baton-snowflake: prefix. The callers in rsa.go / programmatic_access_tokens.go still add it.

I scanned the full PR diff for security and correctness. Repo-local criteria were applied:

  • S1/S5/R7: still satisfied. Only 003001/002003/000606 make the last-used dates unavailable; every other error fails List. The new early return emits nothing only for users that have no keys.
  • L7: both warnings are one-shot (cached status / sync.Once).
  • E2: codes unchanged.
  • J/T/provisioning: not affected.

The tests now pass a real in-memory session store and cover the nil-session skip and the user-without-keys skip. The incremental diff was complete (not partial).

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (medium confidence, accepted risk documented at pkg/snowflake/credentials.go:119-121), pkg/snowflake/credentials.go:136-146:
    • The statement result is fetched with one follow-up GET and is never polled.
    • If that GET still returns a pending/202 response, it parses as zero rows and the status is cached as loaded.
    • Every RSA key and PAT then syncs without a last-used date, and no warning is logged.
    • Cheap mitigation: detect the still-pending response and either log a one-shot Warn or cache the status as unavailable.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/snowflake/credentials.go`:
- Around line 136-146: After the single follow-up GET for the statement result, if Snowflake still reports the statement as running (HTTP 202 / no resultSetMetadata), do not treat it as an empty successful result. Either return a sentinel (e.g. errStatementPending) that loadCredentials maps to credentialsStatusUnavailable with a one-shot Warn ("secret last-used dates unavailable; credentials query did not finish in time"), or poll the handle a bounded number of times with ctx-aware backoff before giving up.

Reviewed commit: 7995db933eee

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

Comment thread pkg/connector/rsa.go

l.Debug("UserRsa", zap.Any("user", user))

if user.RsaPublicKeyFp == "" && user.RsaPublicKeyFp2 == "" &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, fine as a follow-up. This check could move up a level so the RSA and PAT child List calls never run for users without credentials.

RSA: SHOW USERS already returns has_rsa_public_key, and we parse it into User.HasRSAPublicKey. userResource adds the RSA ChildResourceType annotation for every user when secrets sync is on (users.go:247), so every user costs one DESCRIBE USER. Gating it would skip that for keyless users:

if secrets.syncSecrets && user.HasRSAPublicKey {
    opts = append(opts, rs.WithAnnotation(&v2.ChildResourceType{ResourceTypeId: rsaPublicKeyResourceType.Id}))
}

Users with a key still need DESCRIBE USER, since the fingerprints and set times only come from there. This early return would stay as a safety net.

PATs: SHOW USERS also has a has_pat column ("If TRUE, the user has one or more programmatic access tokens", docs). We don't parse it today. Gating the PAT annotation on it would skip SHOW USER PROGRAMMATIC ACCESS TOKENS for users without tokens. Two things to handle:

  • Read it as an optional column, like created_on in ListProgrammaticAccessTokens, and default to true when it's missing. Adding it to userStructFieldToColumnMap would make ParseRow fail on accounts that don't return it.
  • DESCRIBE USER doesn't return it, so GetUser (used by CreateAccount) should also default to true.

Worth checking on a test account first:

  • Is has_rsa_public_key true when only RSA_PUBLIC_KEY_2 is set?
  • Does has_pat count disabled or expired tokens?

If either is false, gating would drop those credentials from the sync.

@agustin-conductor agustin-conductor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment about optionally skipping child resource type for users is a non blocking suggestion, it is technically pre-existing this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants