feat(recipe): push owner-token guard down to pkg/recipe.RecipeResult#1113
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Enterprise Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR implements an ownership-token guard on Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Coverage Report ✅
Coverage BadgeNo Go source files changed in this PR. |
Closes #1080. Add an unexported owner *Builder field to recipe.RecipeResult, stamped by Builder.buildWithStore at build time, plus an AssertOwnedBy(b *Builder) method that returns ErrCodeInvalidRequest when the result was produced by a different Builder (or constructed outside Builder). This pushes the cross-Builder safety guard from the aicr facade (pkg/client/v1.assertOwns in aicr.go) down to the layer where the data lives, so direct pkg/recipe.Builder importers get the same protection. The facade-level check stays as a friendlier error at the facade boundary; this adds defense-in-depth at the recipe layer. Implementation: - recipe.RecipeResult gains unexported owner *Builder field next to the existing provider field. Pointer identity is the token — zero-cost, naturally unique, unforgeable from outside the package. - Builder.buildWithStore stamps result.owner = b before returning, covering both BuildFromCriteria and BuildFromCriteriaWithEvaluator via the shared codepath. - (*RecipeResult).Owner() returns the producer Builder for comparison. - (*RecipeResult).AssertOwnedBy(b) returns nil for the producer, ErrCodeInvalidRequest with the producer/expected %p pointers in context for any other Builder. Nil-safe on the receiver (vacuously owned); nil b argument is rejected (must specify what to assert against); nil owner on a non-nil result is rejected (no provenance, e.g., LoadFromFile path). - DeepCopy leaves owner nil so adopted-recipe flows (AdoptRecipe) cannot pass deep-copies back through ownership-checked entry points without re-building, mirroring how provider is also left nil. Tests in pkg/recipe/builder_test.go: - TestRecipeResult_OwnerStampedByBuilder — happy path. - TestRecipeResult_AssertOwnedBy_RejectsCrossBuilder — analog of pkg/client/v1's TestClient_CrossClientBundleRejected, exercises the rejection without going through the facade. - TestRecipeResult_AssertOwnedBy_NilCases — pins documented nil handling. - TestRecipeResult_DeepCopy_DropsOwner — pins the deep-copy contract. No facade or in-tree caller changes. Existing aicr.assertOwns continues to function as before; the new recipe-level check is additive defense for external pkg/recipe.Builder importers and a foundation for any future pkg/recipe-level enforcement (e.g., GetValuesForComponent checks).
56ef865 to
f6f901b
Compare
Summary
Push the cross-Builder safety guard from the
aicrfacade down torecipe.RecipeResult, so directpkg/recipe.Builderimporters get the same protection the facade callers already have.Motivation / Context
pkg/client/v1.assertOwnsinaicr.gorejects aRecipeResultproduced byClient Afrom being passed toClient B.BundleComponents/ValidateState. That guard lives only at the facade layer; directpkg/recipe.Builderconsumers (which thepkg/recipepackage's "Public (evolving)" tier explicitly allows) get no protection. After the per-Builder isolation work in #1107, any external project that constructs two Builders today (e.g., for tenant isolation) hits the same bug class once they trust the per-Builder routing.Push the guard to the layer where the data lives so it covers both call paths.
Fixes: #1080
Related: #1076 (epic — this is the last open child)
Type of Change
Component(s) Affected
cmd/aicr,pkg/cli)cmd/aicrd,pkg/api,pkg/server)pkg/recipe)pkg/bundler,pkg/component/*)pkg/collector,pkg/snapshotter)pkg/validator)pkg/errors,pkg/k8s)docs/,examples/)Implementation Notes
recipe.RecipeResultgains unexportedowner *Builderfield next to the existingproviderfield. Pointer identity is the token — zero-cost, naturally unique, unforgeable from outside the package.Builder.buildWithStorestampsresult.owner = bbefore returning, covering bothBuildFromCriteriaandBuildFromCriteriaWithEvaluatorvia the shared code path.(*RecipeResult).Owner() *Builderreturns the producer for comparison.(*RecipeResult).AssertOwnedBy(b *Builder) errorreturnsnilfor the producer;ErrCodeInvalidRequest(with producer/expected%ppointers in context) for any other Builder. Nil-safe on the receiver (vacuously owned); nilbargument is rejected (must specify what to assert against); nil owner on a non-nil result is rejected (no provenance — e.g.,LoadFromFilepath).DeepCopyleavesownernil — mirrors howprovideris also left nil. Adopted-recipe flows (AdoptRecipe) cannot pass deep-copies back through ownership-checked entry points without re-building.No facade or in-tree caller changes. The existing
pkg/client/v1.assertOwnscontinues to function as before; the new recipe-level check is additive defense-in-depth for externalpkg/recipe.Builderimporters and a foundation for futurepkg/recipe-level enforcement (e.g.,GetValuesForComponentchecks).Testing
New tests in
pkg/recipe/builder_test.go:TestRecipeResult_OwnerStampedByBuilder— happy path.TestRecipeResult_AssertOwnedBy_RejectsCrossBuilder— analog ofpkg/client/v1.TestClient_CrossClientBundleRejected; exercises the rejection without going through the facade (satisfies the issue's test acceptance criterion).TestRecipeResult_AssertOwnedBy_NilCases— pins documented nil handling.TestRecipeResult_DeepCopy_DropsOwner— pins the deep-copy contract.Risk Assessment
Rollout notes: External Go importers that hold
*pkg/recipe.Buildercan now callresult.AssertOwnedBy(b)before consuming aRecipeResultto verify provenance. Recommended but optional; existing call patterns continue to work.Checklist
go test -race ./pkg/recipe/...)make lint)git commit -S)