Skip to content

Commit 33f6101

Browse files
committed
fix(controlplane): exempt batch-local policy refs in contract apply
Contract apply resolved every policy and policy-group reference against the persisted registry, so a dry-run apply failed when a contract referenced a policy or policy group created in the same batch that was not yet persisted. Add batch_policy_names and batch_policy_group_names to the apply request and treat references to those names as known instead of resolving them, while still validating remote references in both dry-run and real applies. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: f56037fd-1000-4118-9de7-d532c82f30a2
1 parent 4abc341 commit 33f6101

9 files changed

Lines changed: 268 additions & 21 deletions

app/controlplane/api/controlplane/v1/workflow_contract.pb.go

Lines changed: 27 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/controlplane/v1/workflow_contract.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ message WorkflowContractServiceApplyRequest {
121121
bytes raw_schema = 1;
122122
// When true, validate and compute the result without persisting any change
123123
bool dry_run = 2;
124+
// Names of policies created/updated in the same batch apply. References to these are
125+
// treated as known instead of being resolved against the registry (they may not be
126+
// persisted yet, e.g. during dry-run). Remote references are still validated.
127+
repeated string batch_policy_names = 3;
128+
// Same as batch_policy_names, for policy groups created/updated in the same batch.
129+
repeated string batch_policy_group_names = 4;
124130
}
125131

126132
message WorkflowContractServiceApplyResponse {

app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts

Lines changed: 47 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/service/attestation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP
785785

786786
// contract validation
787787
if req.GetContractBytes() != nil {
788-
if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token); err != nil {
788+
if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token, nil, nil); err != nil {
789789
return nil, handleUseCaseErr(err, s.log)
790790
}
791791
}

app/controlplane/internal/service/workflowcontract.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo
146146
}
147147

148148
if len(req.RawContract) != 0 {
149-
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil {
149+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil {
150150
return nil, handleUseCaseErr(err, s.log)
151151
}
152152
}
@@ -212,7 +212,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo
212212

213213
// Validate the contract policies if the raw contract is provided
214214
if len(req.RawContract) != 0 {
215-
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil {
215+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil {
216216
return nil, handleUseCaseErr(err, s.log)
217217
}
218218
}
@@ -253,7 +253,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon
253253
return nil, err
254254
}
255255

256-
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token); err != nil {
256+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token, req.GetBatchPolicyNames(), req.GetBatchPolicyGroupNames()); err != nil {
257257
return nil, handleUseCaseErr(err, s.log)
258258
}
259259

app/controlplane/internal/service/workflowcontract_integration_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,106 @@ spec:
138138
}
139139
}
140140

141+
// contractWithPolicyRef builds a v2 contract that references a policy via a provider scheme.
142+
func contractWithPolicyRef(name, policyRef string) string {
143+
return `
144+
apiVersion: chainloop.dev/v1
145+
kind: Contract
146+
metadata:
147+
name: ` + name + `
148+
spec:
149+
materials:
150+
- type: ARTIFACT
151+
name: my-artifact
152+
policies:
153+
attestation:
154+
- ref: ` + policyRef + `
155+
`
156+
}
157+
158+
// contractWithPolicyGroupRef builds a v2 contract that references a policy group via a provider scheme.
159+
func contractWithPolicyGroupRef(name, groupRef string) string {
160+
return `
161+
apiVersion: chainloop.dev/v1
162+
kind: Contract
163+
metadata:
164+
name: ` + name + `
165+
spec:
166+
materials:
167+
- type: ARTIFACT
168+
name: my-artifact
169+
policyGroups:
170+
- ref: ` + groupRef + `
171+
`
172+
}
173+
174+
// TestApplyBatchExemption verifies that references to resources declared as part of the same
175+
// batch apply are treated as known (not resolved against the registry), while references to
176+
// resources not in the batch are still validated - in dry-run as well as on a real apply.
177+
// No policy provider is configured in this harness, so any non-exempt provider-scheme reference
178+
// fails resolution, which is exactly what proves remote references are still validated.
179+
func (s *workflowContractApplyIntegrationTestSuite) TestApplyBatchExemption() {
180+
testCases := []struct {
181+
name string
182+
rawSchema string
183+
batchPolicyNames []string
184+
batchPolicyGroupNames []string
185+
dryRun bool
186+
wantErr bool
187+
}{
188+
{
189+
name: "batch-local policy exempted in dry-run",
190+
rawSchema: contractWithPolicyRef("svc-apply-batch-pol-dry", "chainloop://batch-pol"),
191+
batchPolicyNames: []string{"batch-pol"},
192+
dryRun: true,
193+
},
194+
{
195+
name: "batch-local policy exempted on real apply",
196+
rawSchema: contractWithPolicyRef("svc-apply-batch-pol-real", "chainloop://batch-pol"),
197+
batchPolicyNames: []string{"batch-pol"},
198+
dryRun: false,
199+
},
200+
{
201+
name: "batch-local policy group exempted in dry-run",
202+
rawSchema: contractWithPolicyGroupRef("svc-apply-batch-grp-dry", "chainloop://batch-grp"),
203+
batchPolicyGroupNames: []string{"batch-grp"},
204+
dryRun: true,
205+
},
206+
{
207+
name: "non-batch policy still validated in dry-run",
208+
rawSchema: contractWithPolicyRef("svc-apply-remote-pol-dry", "chainloop://not-in-batch"),
209+
dryRun: true,
210+
wantErr: true,
211+
},
212+
{
213+
name: "non-batch policy group still validated in dry-run",
214+
rawSchema: contractWithPolicyGroupRef("svc-apply-remote-grp-dry", "chainloop://not-in-batch-grp"),
215+
dryRun: true,
216+
wantErr: true,
217+
},
218+
}
219+
220+
for _, tc := range testCases {
221+
s.Run(tc.name, func() {
222+
resp, err := s.svc.Apply(s.ctx, &pb.WorkflowContractServiceApplyRequest{
223+
RawSchema: []byte(tc.rawSchema),
224+
DryRun: tc.dryRun,
225+
BatchPolicyNames: tc.batchPolicyNames,
226+
BatchPolicyGroupNames: tc.batchPolicyGroupNames,
227+
})
228+
229+
if tc.wantErr {
230+
s.Require().Error(err)
231+
return
232+
}
233+
234+
s.Require().NoError(err)
235+
s.Equal(pb.WorkflowContractServiceApplyResponse_APPLY_STATUS_CREATED, resp.GetStatus())
236+
s.True(resp.GetChanged())
237+
})
238+
}
239+
}
240+
141241
func TestWorkflowContractApply(t *testing.T) {
142242
suite.Run(t, new(workflowContractApplyIntegrationTestSuite))
143243
}

0 commit comments

Comments
 (0)