Add persistent DCRCredentialStore types and memory backend#5186
Add persistent DCRCredentialStore types and memory backend#5186tgrunnagle wants to merge 1 commit intoissue_5040_dcr-2dfrom
Conversation
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
PR size has been reduced below the XL threshold. Thank you for splitting this up!
|
✅ PR size has been reduced below the XL threshold. The size review has been dismissed and this PR can now proceed with normal review. Thank you for splitting this up! |
17bd2ab to
c0fed52
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## issue_5040_dcr-2d #5186 +/- ##
=====================================================
- Coverage 67.74% 67.71% -0.03%
=====================================================
Files 607 607
Lines 62049 62078 +29
=====================================================
+ Hits 42033 42039 +6
- Misses 16854 16876 +22
- Partials 3162 3163 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
64feb73 to
ee7d4cc
Compare
Phase 3 sub-issue 1 of #5183. Define the persisted DCRCredentials value type and the storage-level DCRCredentialStore interface in pkg/authserver/storage/, and ship the in-process memory implementation that single-replica deployments and unit tests use. The Redis backend (sub-issue 2) and the wiring change (sub-issue 3) build on this. DCRKey consolidation: chose option (a) from the issue — DCRKey and its ScopesHash constructor move to pkg/authserver/storage/ so any future backend hashes keys identically. The runner package keeps a package-local type alias (type DCRKey = storage.DCRKey) and a var binding for scopesHash so existing call sites compile unchanged; the canonical form has a single source of truth. DCRCredentials carries ClientSecretExpiresAt so the Redis backend can drive a SetEX TTL without re-touching the value type or regenerating mocks. The interface contract documents this as SHOULD honor when backend-supported; MemoryStorage retains entries verbatim for the process lifetime. StoreDCRCredentials rejects nil creds and zero-valued Key.Issuer or Key.RedirectURI with fosite.ErrInvalidRequest, matching the StoreUpstreamTokens validation pattern. Stats reports dcrCredentials count for parity with the other in-memory maps. The runner-side DCRCredentialStore (Get/Put *DCRResolution) is left in place as the thin adapter sub-issue 3 will swap. This sub-issue lands the new storage-level interface, MemoryStorage implementation, and regenerated mock without touching the wire-up. DCR credentials are intentionally excluded from cleanupExpired: RFC 7591 client registrations are long-lived and the authoritative expiry signal is client_secret_expires_at, which the Redis backend will honor as a SetEX TTL.
c0fed52 to
cc92472
Compare
DRAFT - not ready for review
Summary
pkg/authserver/storage/so bothMemoryStorageand the futureRedisStorage(sub-issue 2) can implement the same interface. Until that happens, every authserver replica re-registers its own DCR client on boot, noclient_secret_expires_atTTL is honored, and the runner-side stub from Authserver DCR integration (Phase 2, Steps 2a-2g) #4978 has no production-shaped sibling. This PR ships the storage-level interface, the value type with full RFC 7591 + 7592 fields, and the in-memory backend; the Redis backend and the wiring swap follow as separate PRs.DCRCredentialsvalue type inpkg/authserver/storage/types.gocarrying the canonicalDCRKey,ClientID/ClientSecret,TokenEndpointAuthMethod, RFC 7592 management fields (RegistrationAccessToken,RegistrationClientURI),AuthorizationEndpoint/TokenEndpoint,CreatedAt, andClientSecretExpiresAt(added in the review-feedback commit so the Redis backend can satisfy the TTL contract without re-touching the value type).DCRCredentialStoreinterface (GetDCRCredentials,StoreDCRCredentials) added to the package'smockgendirective; mocks regenerated.DCRKeyand its canonicalScopesHashconstructor moved frompkg/authserver/runner/intopkg/authserver/storage/. The runner keeps a package-localtype DCRKey = storage.DCRKeyalias and avar scopesHash = storage.ScopesHashbinding so existing call sites compile unchanged and the canonical hashing has a single source of truth.MemoryStoragegains adcrCredentials map[DCRKey]*DCRCredentialsguarded by the existingsync.RWMutex;StoreDCRCredentialsdefensively copies on write and rejects creds with emptyKey.IssuerorKey.RedirectURI;GetDCRCredentialsdefensively copies on read and returns wrappedErrNotFoundon miss. DCR entries are intentionally excluded fromcleanupExpired(registrations are long-lived; Redis will useSetEXfor the TTL case).Statsexposes the new map's count for parity with the other in-memory maps.DCRUpstreamConfigtype onOAuth2UpstreamConfig(discoveryUrl,registrationEndpoint,initialAccessTokenRef,softwareId,softwareStatement), CEL validation enforcing exactly-one-ofclientId/dcrConfigand exactly-one-ofdiscoveryUrl/registrationEndpoint, controllerutil conversion that resolvesInitialAccessTokenRefto aTOOLHIVE_UPSTREAM_DCR_INITIAL_ACCESS_TOKEN_*env var, regenerated CRD YAML andcrd-api.md.EmbeddedAuthServerowns an in-memoryDCRCredentialStoreand callsresolveDCRCredentialsfor any OAuth2 upstream withDCRConfig; resolvedClientSecretis overlaid through a pairedapplyResolution/applyResolutionToOAuth2Confighelper afterbuildPureOAuth2Config. The caller'sRunConfig.Upstreamsslice is shallow-copied and the OAuth2 sub-config deep-copied before resolution so the original is not mutated. Structured logs added at the resolver and/oauth/registerboundary; per-step error context flows throughdcrStepErrorand is logged once at the boundary./registererror body at 8 KiB before logging (Wire authserver DCR resolver and add structured logs #5044 F2); restore the documented CIMD precedence inremoteAuthLogContext(Wire authserver DCR resolver and add structured logs #5044 F3, F26); hoist the DCR remediationWarnout of theisTransientNetworkErrorclassifier intomarkAsUnauthenticatedso it fires at most once per state transition and only when aclient_idis available (Wire authserver DCR resolver and add structured logs #5044 F5); validateAuthorizationServerConfiginNewHandlerand demote the/oauth/registersuccess log from Info to Debug (Wire authserver DCR resolver and add structured logs #5044 F6, F7); URL-sanitiser hardening insanitizeErrorForLog(strips userinfo + fragment, case-insensitive scheme match, preserves trailing punctuation), lowercased internaldcrStepErrortypes, renamedapplyResolutiontoconsumeResolutionto communicate one-shot semantics, and assorted doc-comment cleanups (Wire authserver DCR resolver and add structured logs #5044 F1, F4, F11, F12, F13, F18, F19, F20, F21, F22, F23, F24, F25).Closes #5183
Type of change
Test plan
task test)task test-e2e)task lint-fix)Specifically:
pkg/authserver/storage/memory_test.go— round-trip on allDCRCredentialsfields includingClientSecretExpiresAt; two distinctDCRKeyvalues do not collide; overwrite semantics replace the prior entry;Geton an absent key returns wrappedErrNotFoundviaerrors.Is; defensive-copy assertions on both the read and write paths;StoreDCRCredentialsrejects emptyKey.Issuerand emptyKey.RedirectURI;Statsreports theDCRCredentialscount.pkg/authserver/storage/types_test.go— canonicalScopesHashexercised here as the single source of truth (the runner-sideTestScopesHash_*cases were dropped in the review-feedback commit).pkg/authserver/runner/dcr_test.go,embeddedauthserver_test.go— full DCR boot path against a mock AS, cache-hit short-circuit issues zero additional HTTP requests, caller'sRunConfig.Upstreamsslice element is unchanged across both calls, sanitiser test cases for userinfo/fragment/case-insensitive scheme/trailing punctuation, single-emission of the panic-recovery log.cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types_test.go,controllerutil/authserver_test.go— table-driven CEL/Go validation for DCR + ClientID conflict, both endpoints set, neither endpoint set, valid single-endpoint cases, andInitialAccessTokenRefenv-var wiring.task genrerun;pkg/authserver/storage/mocks/mock_storage.goregenerated.API Compatibility
The CRD changes add new optional fields on
OAuth2UpstreamConfig(dcrConfig) and add a CEL validation that requires exactly one ofclientIdordcrConfig. Existing manifests that setclientIdcontinue to validate; the newdcrConfigpath is additive.clientIdis now optional at the CRD level (it was previously required), which is a relaxation, not a break.v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
pkg/authserver/storage/types.goDCRCredentials,DCRCredentialStore, moveDCRKey+ScopesHashhere, extendmockgendirectivepkg/authserver/storage/memory.godcrCredentialsmap,StoreDCRCredentials/GetDCRCredentials, defensive-copy helpers, exclude fromcleanupExpired, surface inStatspkg/authserver/storage/memory_test.gopkg/authserver/storage/mocks/mock_storage.gopkg/authserver/runner/dcr_store.goDCRKeybecomes a type alias tostorage.DCRKey;scopesHashrebound tostorage.ScopesHashpkg/authserver/runner/dcr.godcrStepErrorboundary logging,applyResolution/applyResolutionToOAuth2Configpairing, sanitiser hardeningpkg/authserver/runner/dcr_store_test.goScopesHashtests; the canonical suite lives in the storage packagepkg/authserver/runner/embeddedauthserver.gopkg/authserver/server/handlers/handler.goAuthorizationServerConfiginNewHandler; simplifyissuer()pkg/authserver/server/handlers/dcr.gosoftware_idpkg/authserver/server/registration/dcr.gosoftware_idlength, require printable ASCIIpkg/auth/monitored_token_source.goclient_idto constructor parameters; emit DCR remediationWarnonce per state transition frommarkAsUnauthenticatedpkg/oauthproto/dcr.go/registererror body read at 8 KiBpkg/runner/runner.goclient_idfromRemoteAuthConfig(CIMD precedence) into theMonitoredTokenSourcecmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.goDCRUpstreamConfigtype, CEL validation,ValidateOAuth2DCRConfigcmd/thv-operator/pkg/controllerutil/authserver.goDCRConfigtoauthserver.DCRUpstreamConfig; resolveInitialAccessTokenRefto env vardeploy/charts/operator-crds/{files,templates}/crds/*.yaml,docs/operator/crd-api.mdDoes this introduce a user-facing change?
Yes. Kubernetes operators can now configure RFC 7591 Dynamic Client Registration on
OAuth2UpstreamConfigvia a newdcrConfigfield (discoveryUrl/registrationEndpoint,initialAccessTokenRef,softwareId,softwareStatement). Until the Redis backend (Phase 3 sub-issue 2) and the wiring swap (sub-issue 3) land, the storage-levelDCRCredentialStoreintroduced here is implemented only byMemoryStorage, so each authserver replica still holds an independent map; cross-replica sharing arrives with the Redis backend.Special notes for reviewers
94fe0dc7,17bd2ab5) implement Persistent DCRCredentialStore: types, interface, and in-memory backend #5183 and are the primary review target. The earlier six commits are Phase 2 follow-ups (Authserver DCR: wire resolver into authserver and add structured logs (Phase 2, Steps 2d/2g) #5039, Authserver DCR: expose DCR config in operator CRD (Phase 2, CRD surface) #5040, Wire authserver DCR resolver and add structured logs #5044 review feedback) that landed on the same branch because they touch the same DCR call-graph; reviewers may find it easier to read commit-by-commit than diff-by-diff againstmain.DCRKeyconsolidation choice. Issue Persistent DCRCredentialStore: types, interface, and in-memory backend #5183 offered two options: moveDCRKeytostorage/(option a) or keep it inrunner/with a storage-side alias (option b). This PR takes option (a) — canonical definition instorage/, package-local alias inrunner/— so any future Redis backend hashes keys identically. The runner-sidescopesHashis avarbinding (var scopesHash = storage.ScopesHash) rather than a re-export, which keeps the indirection visible to callers.DCRCredentialStoreis intentionally NOT removed. Sub-issue 1 lands the new storage-level interface and implementation; the runner-side adapter (Get/Puton*DCRResolution) is left in place as the thin abstraction the wiring PR (sub-issue 3) will swap. Two competing implementations exist onmainafter this lands — that is expected and called out in the issue.ClientSecretExpiresAton the in-memory backend is informational. The in-memory backend retains the field verbatim and does not act on it; the resolver re-checks expiry on read. The Redis backend will plumb it throughSetEX. The interface contract usesSHOULD(notMUST) for TTL handling so backends without a native TTL remain conformant.UpstreamTokens.StoreDCRCredentialsandGetDCRCredentialsboth do field-by-field copies;DCRCredentialsis a flat value type with no nested maps/slices/pointers so a shallow copy is a deep copy here. If a future field introduces nested mutable state,cloneDCRCredentialsis the single point to update.client_secret,registration_access_token,initial_access_tokenand refresh tokens are never arguments toslog.*calls; the grep assertion from Authserver DCR integration (Phase 2, Steps 2a-2g) #4978 still passes.Implementation plan
Approved implementation plan (issue #5183)
DCRCredentialsvalue type topkg/authserver/storage/types.goadjacent toUpstreamTokens. Mirror the defensive-copy contract documented forUpstreamTokens. Capture RFC 7591 fields plus the RFC 7592 management fields verbatim.DCRKeyand itsScopesHashconstructor frompkg/authserver/runner/topkg/authserver/storage/so any backend hashes keys identically. Keep a package-local type alias in the runner so existing call sites compile unchanged.DCRCredentialStoreinterface (GetDCRCredentials,StoreDCRCredentials) totypes.go. Wire it into the existingmockgendirective. Keep it segregated — do NOT add it to theStorageumbrella interface.MemoryStoragewith adcrCredentials map[DCRKey]*DCRCredentialsguarded by the existingsync.RWMutex. Defensive copy on read and write; reject emptyKey.Issuer/Key.RedirectURI; return wrappedErrNotFoundon miss; exclude fromcleanupExpired; surface inStats. Add the compile-time interface assertion at the bottom of the file.task gento regenerate theDCRCredentialStoremock.task build,task test,task lint-fix,task license-check. Run the secret-grep assertion from Authserver DCR integration (Phase 2, Steps 2a-2g) #4978.Out of scope (deferred to sub-issues 2 and 3):
EmbeddedAuthServerto use the storage-level interface and removing the Phase 2 standalone in-memory stub.