diff --git a/app/controlplane/api/controlplane/v1/workflow_contract.pb.go b/app/controlplane/api/controlplane/v1/workflow_contract.pb.go index 6057f1000..7e781bb3a 100644 --- a/app/controlplane/api/controlplane/v1/workflow_contract.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_contract.pb.go @@ -573,9 +573,15 @@ type WorkflowContractServiceApplyRequest struct { // Raw representation of the contract in json, yaml or cue RawSchema []byte `protobuf:"bytes,1,opt,name=raw_schema,json=rawSchema,proto3" json:"raw_schema,omitempty"` // When true, validate and compute the result without persisting any change - DryRun bool `protobuf:"varint,2,opt,name=dry_run,json=dryRun,proto3" json:"dry_run,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + DryRun bool `protobuf:"varint,2,opt,name=dry_run,json=dryRun,proto3" json:"dry_run,omitempty"` + // Names of policies created/updated in the same batch apply. References to these are + // treated as known instead of being resolved against the registry (they may not be + // persisted yet, e.g. during dry-run). Remote references are still validated. + BatchPolicyNames []string `protobuf:"bytes,3,rep,name=batch_policy_names,json=batchPolicyNames,proto3" json:"batch_policy_names,omitempty"` + // Same as batch_policy_names, for policy groups created/updated in the same batch. + BatchPolicyGroupNames []string `protobuf:"bytes,4,rep,name=batch_policy_group_names,json=batchPolicyGroupNames,proto3" json:"batch_policy_group_names,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WorkflowContractServiceApplyRequest) Reset() { @@ -622,6 +628,20 @@ func (x *WorkflowContractServiceApplyRequest) GetDryRun() bool { return false } +func (x *WorkflowContractServiceApplyRequest) GetBatchPolicyNames() []string { + if x != nil { + return x.BatchPolicyNames + } + return nil +} + +func (x *WorkflowContractServiceApplyRequest) GetBatchPolicyGroupNames() []string { + if x != nil { + return x.BatchPolicyGroupNames + } + return nil +} + type WorkflowContractServiceApplyResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Result *WorkflowContractItem `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` @@ -852,11 +872,13 @@ const file_controlplane_v1_workflow_contract_proto_rawDesc = "" + "$WorkflowContractServiceDeleteRequest\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\"'\n" + - "%WorkflowContractServiceDeleteResponse\"]\n" + + "%WorkflowContractServiceDeleteResponse\"\xc4\x01\n" + "#WorkflowContractServiceApplyRequest\x12\x1d\n" + "\n" + "raw_schema\x18\x01 \x01(\fR\trawSchema\x12\x17\n" + - "\adry_run\x18\x02 \x01(\bR\x06dryRun\"\xa4\x03\n" + + "\adry_run\x18\x02 \x01(\bR\x06dryRun\x12,\n" + + "\x12batch_policy_names\x18\x03 \x03(\tR\x10batchPolicyNames\x127\n" + + "\x18batch_policy_group_names\x18\x04 \x03(\tR\x15batchPolicyGroupNames\"\xa4\x03\n" + "$WorkflowContractServiceApplyResponse\x12=\n" + "\x06result\x18\x01 \x01(\v2%.controlplane.v1.WorkflowContractItemR\x06result\x12 \n" + "\tunchanged\x18\x02 \x01(\bB\x02\x18\x01R\tunchanged\x12\x18\n" + diff --git a/app/controlplane/api/controlplane/v1/workflow_contract.proto b/app/controlplane/api/controlplane/v1/workflow_contract.proto index ab9fb87fd..4169bdd83 100644 --- a/app/controlplane/api/controlplane/v1/workflow_contract.proto +++ b/app/controlplane/api/controlplane/v1/workflow_contract.proto @@ -121,6 +121,12 @@ message WorkflowContractServiceApplyRequest { bytes raw_schema = 1; // When true, validate and compute the result without persisting any change bool dry_run = 2; + // Names of policies created/updated in the same batch apply. References to these are + // treated as known instead of being resolved against the registry (they may not be + // persisted yet, e.g. during dry-run). Remote references are still validated. + repeated string batch_policy_names = 3; + // Same as batch_policy_names, for policy groups created/updated in the same batch. + repeated string batch_policy_group_names = 4; } message WorkflowContractServiceApplyResponse { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts index 8e74f327e..c60d110e9 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts @@ -71,6 +71,14 @@ export interface WorkflowContractServiceApplyRequest { rawSchema: Uint8Array; /** When true, validate and compute the result without persisting any change */ dryRun: boolean; + /** + * Names of policies created/updated in the same batch apply. References to these are + * treated as known instead of being resolved against the registry (they may not be + * persisted yet, e.g. during dry-run). Remote references are still validated. + */ + batchPolicyNames: string[]; + /** Same as batch_policy_names, for policy groups created/updated in the same batch. */ + batchPolicyGroupNames: string[]; } export interface WorkflowContractServiceApplyResponse { @@ -1001,7 +1009,7 @@ export const WorkflowContractServiceDeleteResponse = { }; function createBaseWorkflowContractServiceApplyRequest(): WorkflowContractServiceApplyRequest { - return { rawSchema: new Uint8Array(0), dryRun: false }; + return { rawSchema: new Uint8Array(0), dryRun: false, batchPolicyNames: [], batchPolicyGroupNames: [] }; } export const WorkflowContractServiceApplyRequest = { @@ -1012,6 +1020,12 @@ export const WorkflowContractServiceApplyRequest = { if (message.dryRun === true) { writer.uint32(16).bool(message.dryRun); } + for (const v of message.batchPolicyNames) { + writer.uint32(26).string(v!); + } + for (const v of message.batchPolicyGroupNames) { + writer.uint32(34).string(v!); + } return writer; }, @@ -1036,6 +1050,20 @@ export const WorkflowContractServiceApplyRequest = { message.dryRun = reader.bool(); continue; + case 3: + if (tag !== 26) { + break; + } + + message.batchPolicyNames.push(reader.string()); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.batchPolicyGroupNames.push(reader.string()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1049,6 +1077,12 @@ export const WorkflowContractServiceApplyRequest = { return { rawSchema: isSet(object.rawSchema) ? bytesFromBase64(object.rawSchema) : new Uint8Array(0), dryRun: isSet(object.dryRun) ? Boolean(object.dryRun) : false, + batchPolicyNames: Array.isArray(object?.batchPolicyNames) + ? object.batchPolicyNames.map((e: any) => String(e)) + : [], + batchPolicyGroupNames: Array.isArray(object?.batchPolicyGroupNames) + ? object.batchPolicyGroupNames.map((e: any) => String(e)) + : [], }; }, @@ -1057,6 +1091,16 @@ export const WorkflowContractServiceApplyRequest = { message.rawSchema !== undefined && (obj.rawSchema = base64FromBytes(message.rawSchema !== undefined ? message.rawSchema : new Uint8Array(0))); message.dryRun !== undefined && (obj.dryRun = message.dryRun); + if (message.batchPolicyNames) { + obj.batchPolicyNames = message.batchPolicyNames.map((e) => e); + } else { + obj.batchPolicyNames = []; + } + if (message.batchPolicyGroupNames) { + obj.batchPolicyGroupNames = message.batchPolicyGroupNames.map((e) => e); + } else { + obj.batchPolicyGroupNames = []; + } return obj; }, @@ -1072,6 +1116,8 @@ export const WorkflowContractServiceApplyRequest = { const message = createBaseWorkflowContractServiceApplyRequest(); message.rawSchema = object.rawSchema ?? new Uint8Array(0); message.dryRun = object.dryRun ?? false; + message.batchPolicyNames = object.batchPolicyNames?.map((e) => e) || []; + message.batchPolicyGroupNames = object.batchPolicyGroupNames?.map((e) => e) || []; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json index 1df8e1e06..1824bcc9e 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json @@ -3,6 +3,20 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(batch_policy_group_names)$": { + "description": "Same as batch_policy_names, for policy groups created/updated in the same batch.", + "items": { + "type": "string" + }, + "type": "array" + }, + "^(batch_policy_names)$": { + "description": "Names of policies created/updated in the same batch apply. References to these are\n treated as known instead of being resolved against the registry (they may not be\n persisted yet, e.g. during dry-run). Remote references are still validated.", + "items": { + "type": "string" + }, + "type": "array" + }, "^(dry_run)$": { "description": "When true, validate and compute the result without persisting any change", "type": "boolean" @@ -14,6 +28,20 @@ } }, "properties": { + "batchPolicyGroupNames": { + "description": "Same as batch_policy_names, for policy groups created/updated in the same batch.", + "items": { + "type": "string" + }, + "type": "array" + }, + "batchPolicyNames": { + "description": "Names of policies created/updated in the same batch apply. References to these are\n treated as known instead of being resolved against the registry (they may not be\n persisted yet, e.g. during dry-run). Remote references are still validated.", + "items": { + "type": "string" + }, + "type": "array" + }, "dryRun": { "description": "When true, validate and compute the result without persisting any change", "type": "boolean" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json index 3a9060d0d..15856b96a 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json @@ -3,6 +3,20 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(batchPolicyGroupNames)$": { + "description": "Same as batch_policy_names, for policy groups created/updated in the same batch.", + "items": { + "type": "string" + }, + "type": "array" + }, + "^(batchPolicyNames)$": { + "description": "Names of policies created/updated in the same batch apply. References to these are\n treated as known instead of being resolved against the registry (they may not be\n persisted yet, e.g. during dry-run). Remote references are still validated.", + "items": { + "type": "string" + }, + "type": "array" + }, "^(dryRun)$": { "description": "When true, validate and compute the result without persisting any change", "type": "boolean" @@ -14,6 +28,20 @@ } }, "properties": { + "batch_policy_group_names": { + "description": "Same as batch_policy_names, for policy groups created/updated in the same batch.", + "items": { + "type": "string" + }, + "type": "array" + }, + "batch_policy_names": { + "description": "Names of policies created/updated in the same batch apply. References to these are\n treated as known instead of being resolved against the registry (they may not be\n persisted yet, e.g. during dry-run). Remote references are still validated.", + "items": { + "type": "string" + }, + "type": "array" + }, "dry_run": { "description": "When true, validate and compute the result without persisting any change", "type": "boolean" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 61815fbd2..bfeed5266 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -785,7 +785,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP // contract validation if req.GetContractBytes() != nil { - if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token); err != nil { + if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token, nil, nil); err != nil { return nil, handleUseCaseErr(err, s.log) } } diff --git a/app/controlplane/internal/service/workflowcontract.go b/app/controlplane/internal/service/workflowcontract.go index 6debe45b7..6769b8908 100644 --- a/app/controlplane/internal/service/workflowcontract.go +++ b/app/controlplane/internal/service/workflowcontract.go @@ -146,7 +146,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo } if len(req.RawContract) != 0 { - if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil { + if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil { return nil, handleUseCaseErr(err, s.log) } } @@ -212,7 +212,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo // Validate the contract policies if the raw contract is provided if len(req.RawContract) != 0 { - if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil { + if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil { return nil, handleUseCaseErr(err, s.log) } } @@ -253,7 +253,16 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon return nil, err } - if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token); err != nil { + // Batch-local references are only exempted from registry resolution on a dry-run, where + // the resources may not be persisted yet. A real apply persists batch resources before the + // contract, so it must always validate fully and never trust the client-supplied batch lists. + var batchPolicyNames, batchPolicyGroupNames []string + if dryRun { + batchPolicyNames = req.GetBatchPolicyNames() + batchPolicyGroupNames = req.GetBatchPolicyGroupNames() + } + + if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token, batchPolicyNames, batchPolicyGroupNames); err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/internal/service/workflowcontract_integration_test.go b/app/controlplane/internal/service/workflowcontract_integration_test.go index 06a2c7e3a..b6eb69ebd 100644 --- a/app/controlplane/internal/service/workflowcontract_integration_test.go +++ b/app/controlplane/internal/service/workflowcontract_integration_test.go @@ -138,6 +138,118 @@ spec: } } +// contractWithPolicyRef builds a v2 contract that references a policy via a provider scheme. +func contractWithPolicyRef(name, policyRef string) string { + return ` +apiVersion: chainloop.dev/v1 +kind: Contract +metadata: + name: ` + name + ` +spec: + materials: + - type: ARTIFACT + name: my-artifact + policies: + attestation: + - ref: ` + policyRef + ` +` +} + +// contractWithPolicyGroupRef builds a v2 contract that references a policy group via a provider scheme. +func contractWithPolicyGroupRef(name, groupRef string) string { + return ` +apiVersion: chainloop.dev/v1 +kind: Contract +metadata: + name: ` + name + ` +spec: + materials: + - type: ARTIFACT + name: my-artifact + policyGroups: + - ref: ` + groupRef + ` +` +} + +// TestApplyBatchExemption verifies that, on a dry-run, bare references to resources declared as +// part of the same batch apply are treated as known (not resolved against the registry), while +// remote references, explicitly scoped references, and any reference on a real apply are still +// validated. No policy provider is configured in this harness, so any non-exempt provider-scheme +// reference fails resolution, which is exactly what proves those references are still validated. +func (s *workflowContractApplyIntegrationTestSuite) TestApplyBatchExemption() { + testCases := []struct { + name string + rawSchema string + batchPolicyNames []string + batchPolicyGroupNames []string + dryRun bool + wantErr bool + }{ + { + name: "batch-local policy exempted in dry-run", + rawSchema: contractWithPolicyRef("svc-apply-batch-pol-dry", "chainloop://batch-pol"), + batchPolicyNames: []string{"batch-pol"}, + dryRun: true, + }, + { + name: "batch-local policy group exempted in dry-run", + rawSchema: contractWithPolicyGroupRef("svc-apply-batch-grp-dry", "chainloop://batch-grp"), + batchPolicyGroupNames: []string{"batch-grp"}, + dryRun: true, + }, + { + // A real apply persists batch resources before the contract, so it must validate + // fully and never trust the client-supplied batch list. + name: "batch list not trusted on real apply", + rawSchema: contractWithPolicyRef("svc-apply-batch-pol-real", "chainloop://batch-pol"), + batchPolicyNames: []string{"batch-pol"}, + dryRun: false, + wantErr: true, + }, + { + // An org-scoped reference is unambiguously remote and must not be exempted by a + // bare-name collision with a batch-local policy. + name: "org-scoped ref not exempted by name collision", + rawSchema: contractWithPolicyRef("svc-apply-scoped-pol-dry", "chainloop://acme/batch-pol"), + batchPolicyNames: []string{"batch-pol"}, + dryRun: true, + wantErr: true, + }, + { + name: "non-batch policy still validated in dry-run", + rawSchema: contractWithPolicyRef("svc-apply-remote-pol-dry", "chainloop://not-in-batch"), + dryRun: true, + wantErr: true, + }, + { + name: "non-batch policy group still validated in dry-run", + rawSchema: contractWithPolicyGroupRef("svc-apply-remote-grp-dry", "chainloop://not-in-batch-grp"), + dryRun: true, + wantErr: true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + resp, err := s.svc.Apply(s.ctx, &pb.WorkflowContractServiceApplyRequest{ + RawSchema: []byte(tc.rawSchema), + DryRun: tc.dryRun, + BatchPolicyNames: tc.batchPolicyNames, + BatchPolicyGroupNames: tc.batchPolicyGroupNames, + }) + + if tc.wantErr { + s.Require().Error(err) + return + } + + s.Require().NoError(err) + s.Equal(pb.WorkflowContractServiceApplyResponse_APPLY_STATUS_CREATED, resp.GetStatus()) + s.True(resp.GetChanged()) + }) + } +} + func TestWorkflowContractApply(t *testing.T) { suite.Run(t, new(workflowContractApplyIntegrationTestSuite)) } diff --git a/app/controlplane/pkg/biz/workflowcontract.go b/app/controlplane/pkg/biz/workflowcontract.go index 44277dc08..f502cbef2 100644 --- a/app/controlplane/pkg/biz/workflowcontract.go +++ b/app/controlplane/pkg/biz/workflowcontract.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "time" @@ -500,7 +501,12 @@ func (uc *WorkflowContractUseCase) RevisionWouldChange(ctx context.Context, orgI return !bytes.Equal(latest.Version.Schema.Raw, incoming.Raw), nil } -func (uc *WorkflowContractUseCase) ValidateContractPolicies(ctx context.Context, rawSchema []byte, token string) error { +// ValidateContractPolicies checks that policies and policy groups referenced by the contract +// exist. batchPolicyNames and batchPolicyGroupNames carry the names of resources being +// created/updated in the same batch apply: references to those are treated as known instead of +// being resolved against the registry, since they may not be persisted yet (e.g. during +// dry-run). Remote references are still validated regardless. +func (uc *WorkflowContractUseCase) ValidateContractPolicies(ctx context.Context, rawSchema []byte, token string, batchPolicyNames, batchPolicyGroupNames []string) error { // Validate that externally provided policies exist c, err := identifyUnMarshalAndValidateRawContract(rawSchema) if err != nil { @@ -513,17 +519,17 @@ func (uc *WorkflowContractUseCase) ValidateContractPolicies(ctx context.Context, // DEPRECATED: v1 schema is deprecated, use v2 Contract format instead schema := c.Schema for _, att := range schema.GetPolicies().GetAttestation() { - if _, err := uc.findAndValidatePolicy(ctx, att, token); err != nil { + if _, err := uc.findAndValidatePolicy(ctx, att, token, batchPolicyNames); err != nil { return NewErrValidation(err) } } for _, att := range schema.GetPolicies().GetMaterials() { - if _, err := uc.findAndValidatePolicy(ctx, att, token); err != nil { + if _, err := uc.findAndValidatePolicy(ctx, att, token, batchPolicyNames); err != nil { return NewErrValidation(err) } } for _, gatt := range schema.GetPolicyGroups() { - if _, err := uc.findAndValidatePolicyGroup(ctx, gatt, token); err != nil { + if _, err := uc.findAndValidatePolicyGroup(ctx, gatt, token, batchPolicyGroupNames); err != nil { return NewErrValidation(err) } } @@ -532,18 +538,18 @@ func (uc *WorkflowContractUseCase) ValidateContractPolicies(ctx context.Context, spec := c.Schemav2.GetSpec() if spec.GetPolicies() != nil { for _, att := range spec.GetPolicies().GetAttestation() { - if _, err := uc.findAndValidatePolicy(ctx, att, token); err != nil { + if _, err := uc.findAndValidatePolicy(ctx, att, token, batchPolicyNames); err != nil { return NewErrValidation(err) } } for _, att := range spec.GetPolicies().GetMaterials() { - if _, err := uc.findAndValidatePolicy(ctx, att, token); err != nil { + if _, err := uc.findAndValidatePolicy(ctx, att, token, batchPolicyNames); err != nil { return NewErrValidation(err) } } } for _, gatt := range spec.GetPolicyGroups() { - if _, err := uc.findAndValidatePolicyGroup(ctx, gatt, token); err != nil { + if _, err := uc.findAndValidatePolicyGroup(ctx, gatt, token, batchPolicyGroupNames); err != nil { return NewErrValidation(err) } } @@ -570,7 +576,7 @@ func (uc *WorkflowContractUseCase) ValidatePolicyAttachment(ctx context.Context, return nil } -func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, att *schemav1.PolicyAttachment, token string) (*schemav1.Policy, error) { +func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, att *schemav1.PolicyAttachment, token string, batchPolicyNames []string) (*schemav1.Policy, error) { var policy *schemav1.Policy if att.GetEmbedded() != nil { @@ -581,6 +587,13 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, at // [chainloop://][provider:][org_name/]name if loader.IsProviderScheme(att.GetRef()) { pr := loader.ProviderParts(att.GetRef()) + // Policies created/updated in the same batch apply may not be persisted yet, so + // treat references to them as known instead of resolving against the registry. Only + // bare references (no provider/org) can name a batch-local policy; references that + // explicitly target a remote provider or org are always validated. + if pr.Provider == "" && pr.OrgName == "" && slices.Contains(batchPolicyNames, pr.Name) { + return nil, nil + } // Validate attachment if err := uc.ValidatePolicyAttachment(ctx, pr.Provider, att, token); err != nil { return nil, err @@ -608,7 +621,7 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, at return policy, nil } -func (uc *WorkflowContractUseCase) findAndValidatePolicyGroup(ctx context.Context, att *schemav1.PolicyGroupAttachment, token string) (*schemav1.PolicyGroup, error) { +func (uc *WorkflowContractUseCase) findAndValidatePolicyGroup(ctx context.Context, att *schemav1.PolicyGroupAttachment, token string, batchPolicyGroupNames []string) (*schemav1.PolicyGroup, error) { if !loader.IsProviderScheme(att.GetRef()) { // Otherwise, don't return an error, as it might consist of a local policy, not available in this context return nil, nil @@ -617,6 +630,13 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicyGroup(ctx context.Contex // if it should come from a provider, check that it's available // [chainloop://][provider/]name pr := loader.ProviderParts(att.GetRef()) + // Policy groups created/updated in the same batch apply may not be persisted yet, so + // treat references to them as known instead of resolving against the registry. Only bare + // references (no provider/org) can name a batch-local group; references that explicitly + // target a remote provider or org are always validated. + if pr.Provider == "" && pr.OrgName == "" && slices.Contains(batchPolicyGroupNames, pr.Name) { + return nil, nil + } remoteGroup, err := uc.GetPolicyGroup(ctx, pr.Provider, pr.Name, pr.OrgName, "", token) if err != nil { return nil, NewErrValidation(fmt.Errorf("failed to get policy group: %w", err)) @@ -659,7 +679,8 @@ func (uc *WorkflowContractUseCase) validateSkipList(ctx context.Context, group * // Collect material policy names for _, groupMaterial := range policies.GetMaterials() { for _, policyAtt := range groupMaterial.GetPolicies() { - policy, err := uc.findAndValidatePolicy(ctx, policyAtt, token) + // Group member policies come from an already-resolved remote group, never batch-local. + policy, err := uc.findAndValidatePolicy(ctx, policyAtt, token, nil) if err != nil { return fmt.Errorf("failed to get policy name during skip list validation: %w", err) } @@ -669,7 +690,7 @@ func (uc *WorkflowContractUseCase) validateSkipList(ctx context.Context, group * // Collect attestation policy names for _, policyAtt := range policies.GetAttestation() { - policy, err := uc.findAndValidatePolicy(ctx, policyAtt, token) + policy, err := uc.findAndValidatePolicy(ctx, policyAtt, token, nil) if err != nil { return fmt.Errorf("failed to get policy name during skip list validation: %w", err) }