Skip to content

Commit b576973

Browse files
committed
feat(policies): add option to ignore CLI compatibility when resolving policies
Add a WithIgnoreCLICompatibility resolve option that forwards the include_all_versions query parameter to the policy provider, making it skip CLI-version compatibility resolution and return the true latest revision instead of the latest revision compatible with the requesting CLI version. The control-plane contract-save validation path uses the option because it is a non-CLI caller; the attestation RPC path (a CLI caller) keeps the default behavior. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 8efb6e0 commit b576973

3 files changed

Lines changed: 117 additions & 17 deletions

File tree

app/controlplane/pkg/biz/workflowcontract.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,9 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, at
599599
return nil, err
600600
}
601601

602-
remotePolicy, err := uc.GetPolicy(ctx, pr.Provider, pr.Name, pr.OrgName, "", token)
602+
// The control plane is a non-CLI caller, so resolve the true latest revision
603+
// and skip CLI-version compatibility gating.
604+
remotePolicy, err := uc.GetPolicy(ctx, pr.Provider, pr.Name, pr.OrgName, "", token, policies.WithIgnoreCLICompatibility())
603605
if err != nil {
604606
return nil, err
605607
}
@@ -637,7 +639,9 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicyGroup(ctx context.Contex
637639
if pr.Provider == "" && pr.OrgName == "" && slices.Contains(batchPolicyGroupNames, pr.Name) {
638640
return nil, nil
639641
}
640-
remoteGroup, err := uc.GetPolicyGroup(ctx, pr.Provider, pr.Name, pr.OrgName, "", token)
642+
// The control plane is a non-CLI caller, so resolve the true latest revision
643+
// and skip CLI-version compatibility gating.
644+
remoteGroup, err := uc.GetPolicyGroup(ctx, pr.Provider, pr.Name, pr.OrgName, "", token, policies.WithIgnoreCLICompatibility())
641645
if err != nil {
642646
return nil, NewErrValidation(fmt.Errorf("failed to get policy group: %w", err))
643647
}
@@ -779,14 +783,15 @@ func providerAuthOpts(ctx context.Context, token, currentOrgName string) policie
779783
}
780784
}
781785

782-
// GetPolicy retrieves a policy from a policy provider
783-
func (uc *WorkflowContractUseCase) GetPolicy(ctx context.Context, providerName, policyName, policyOrgName, currentOrgName, token string) (*RemotePolicy, error) {
786+
// GetPolicy retrieves a policy from a policy provider.
787+
// See policies.WithIgnoreCLICompatibility for the available resolution options.
788+
func (uc *WorkflowContractUseCase) GetPolicy(ctx context.Context, providerName, policyName, policyOrgName, currentOrgName, token string, opts ...policies.ResolveOption) (*RemotePolicy, error) {
784789
provider, err := uc.findProvider(providerName)
785790
if err != nil {
786791
return nil, err
787792
}
788793

789-
policy, ref, err := provider.Resolve(policyName, policyOrgName, providerAuthOpts(ctx, token, currentOrgName))
794+
policy, ref, err := provider.Resolve(policyName, policyOrgName, providerAuthOpts(ctx, token, currentOrgName), opts...)
790795
if err != nil {
791796
if errors.Is(err, policies.ErrNotFound) {
792797
return nil, NewErrNotFound(fmt.Sprintf("policy %q", policyName))
@@ -801,13 +806,15 @@ func (uc *WorkflowContractUseCase) GetPolicy(ctx context.Context, providerName,
801806
return &RemotePolicy{Policy: policy, ProviderRef: ref}, nil
802807
}
803808

804-
func (uc *WorkflowContractUseCase) GetPolicyGroup(ctx context.Context, providerName, groupName, groupOrgName, currentOrgName, token string) (*RemotePolicyGroup, error) {
809+
// GetPolicyGroup retrieves a policy group from a policy provider.
810+
// See policies.WithIgnoreCLICompatibility for the available resolution options.
811+
func (uc *WorkflowContractUseCase) GetPolicyGroup(ctx context.Context, providerName, groupName, groupOrgName, currentOrgName, token string, opts ...policies.ResolveOption) (*RemotePolicyGroup, error) {
805812
provider, err := uc.findProvider(providerName)
806813
if err != nil {
807814
return nil, err
808815
}
809816

810-
group, ref, err := provider.ResolveGroup(groupName, groupOrgName, providerAuthOpts(ctx, token, currentOrgName))
817+
group, ref, err := provider.ResolveGroup(groupName, groupOrgName, providerAuthOpts(ctx, token, currentOrgName), opts...)
811818
if err != nil {
812819
if errors.Is(err, policies.ErrNotFound) {
813820
return nil, NewErrNotFound(fmt.Sprintf("policy group %q", groupName))

app/controlplane/pkg/policies/policyprovider.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ const (
3636
validateAction = "validate"
3737
groupsEndpoint = "groups"
3838

39-
digestParam = "digest"
40-
orgNameParam = "organization_name"
41-
organizationHeader = "Chainloop-Organization"
39+
digestParam = "digest"
40+
orgNameParam = "organization_name"
41+
// includeAllVersionsParam is the provider's query parameter that, when set,
42+
// makes the provider skip CLI-version compatibility resolution and return the
43+
// true latest revision. The wire name is "include_all_versions" but its real
44+
// effect is "ignore CLI compatibility gating" (see WithIgnoreCLICompatibility).
45+
includeAllVersionsParam = "include_all_versions"
46+
organizationHeader = "Chainloop-Organization"
4247
)
4348

4449
// PolicyProvider represents an external policy provider
@@ -88,8 +93,37 @@ var (
8893
ErrUnauthorized = fmt.Errorf("unauthorized request to policy provider")
8994
)
9095

91-
// Resolve calls the remote provider for retrieving a policy
92-
func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts ProviderAuthOpts) (*schemaapi.Policy, *PolicyReference, error) {
96+
// ResolveOption configures a policy or policy-group resolution request.
97+
type ResolveOption func(*resolveOptions)
98+
99+
type resolveOptions struct {
100+
ignoreCLICompatibility bool
101+
}
102+
103+
func newResolveOptions(opts []ResolveOption) resolveOptions {
104+
var o resolveOptions
105+
for _, opt := range opts {
106+
if opt != nil {
107+
opt(&o)
108+
}
109+
}
110+
return o
111+
}
112+
113+
// WithIgnoreCLICompatibility makes the provider skip CLI-version compatibility
114+
// resolution and return the true latest revision instead of the latest revision
115+
// compatible with the requesting CLI version. It is meant for non-CLI callers
116+
// such as the web UI or the control plane. On the wire this sets the
117+
// include_all_versions query parameter.
118+
func WithIgnoreCLICompatibility() ResolveOption {
119+
return func(o *resolveOptions) {
120+
o.ignoreCLICompatibility = true
121+
}
122+
}
123+
124+
// Resolve calls the remote provider for retrieving a policy.
125+
// See WithIgnoreCLICompatibility for the available resolution options.
126+
func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts ProviderAuthOpts, opts ...ResolveOption) (*schemaapi.Policy, *PolicyReference, error) {
93127
if policyName == "" || authOpts.Token == "" {
94128
return nil, nil, fmt.Errorf("both policyname and auth opts are mandatory")
95129
}
@@ -108,7 +142,7 @@ func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts Prov
108142
}
109143
// we want to override the orgName with the one in the response
110144
// since we might have resolved it implicitly
111-
providerDigest, orgName, err := p.queryProvider(url, digest, policyOrgName, authOpts, &policy)
145+
providerDigest, orgName, err := p.queryProvider(url, digest, policyOrgName, authOpts, &policy, opts...)
112146
if err != nil {
113147
return nil, nil, fmt.Errorf("failed to resolve policy: %w", err)
114148
}
@@ -189,8 +223,9 @@ func (p *PolicyProvider) ValidateAttachment(att *schemaapi.PolicyAttachment, aut
189223
return nil
190224
}
191225

192-
// ResolveGroup calls remote provider for retrieving a policy group definition
193-
func (p *PolicyProvider) ResolveGroup(groupName, groupOrgName string, authOpts ProviderAuthOpts) (*schemaapi.PolicyGroup, *PolicyReference, error) {
226+
// ResolveGroup calls remote provider for retrieving a policy group definition.
227+
// See WithIgnoreCLICompatibility for the available resolution options.
228+
func (p *PolicyProvider) ResolveGroup(groupName, groupOrgName string, authOpts ProviderAuthOpts, opts ...ResolveOption) (*schemaapi.PolicyGroup, *PolicyReference, error) {
194229
if groupName == "" || authOpts.Token == "" {
195230
return nil, nil, fmt.Errorf("both policyname and token are mandatory")
196231
}
@@ -209,7 +244,7 @@ func (p *PolicyProvider) ResolveGroup(groupName, groupOrgName string, authOpts P
209244
}
210245
// we want to override the orgName with the one in the response
211246
// since we might have resolved it implicitly
212-
providerDigest, orgName, err := p.queryProvider(url, digest, groupOrgName, authOpts, &group)
247+
providerDigest, orgName, err := p.queryProvider(url, digest, groupOrgName, authOpts, &group, opts...)
213248
if err != nil {
214249
return nil, nil, fmt.Errorf("failed to resolve group: %w", err)
215250
}
@@ -218,7 +253,9 @@ func (p *PolicyProvider) ResolveGroup(groupName, groupOrgName string, authOpts P
218253
}
219254

220255
// returns digest, orgname, error
221-
func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, authOpts ProviderAuthOpts, out proto.Message) (string, string, error) {
256+
func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, authOpts ProviderAuthOpts, out proto.Message, opts ...ResolveOption) (string, string, error) {
257+
options := newResolveOptions(opts)
258+
222259
query := url.Query()
223260
if digest != "" {
224261
query.Set(digestParam, digest)
@@ -228,6 +265,10 @@ func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, aut
228265
query.Set(orgNameParam, orgName)
229266
}
230267

268+
if options.ignoreCLICompatibility {
269+
query.Set(includeAllVersionsParam, "true")
270+
}
271+
231272
url.RawQuery = query.Encode()
232273

233274
req, err := http.NewRequest("GET", url.String(), nil)

app/controlplane/pkg/policies/policyprovider_http_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,58 @@ func TestProviderForwardsCLIVersionHeader(t *testing.T) {
214214
}
215215
}
216216

217+
func TestResolveForwardsIgnoreCLICompatibility(t *testing.T) {
218+
testCases := []struct {
219+
name string
220+
opts []ResolveOption
221+
wantParam string
222+
}{
223+
{
224+
name: "include_all_versions set when ignoring CLI compatibility",
225+
opts: []ResolveOption{WithIgnoreCLICompatibility()},
226+
wantParam: "true",
227+
},
228+
{
229+
name: "param omitted when no options passed",
230+
opts: nil,
231+
wantParam: "",
232+
},
233+
{
234+
name: "nil option is ignored without panicking",
235+
opts: []ResolveOption{nil},
236+
wantParam: "",
237+
},
238+
}
239+
240+
for _, tc := range testCases {
241+
t.Run(tc.name, func(t *testing.T) {
242+
var policyParam, groupParam string
243+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
244+
got := r.URL.Query().Get("include_all_versions")
245+
if strings.Contains(r.URL.Path, "/groups/") {
246+
groupParam = got
247+
} else {
248+
policyParam = got
249+
}
250+
w.WriteHeader(http.StatusOK)
251+
_, _ = w.Write([]byte(`{}`))
252+
}))
253+
defer server.Close()
254+
255+
provider := &PolicyProvider{name: "test", url: server.URL}
256+
authOpts := ProviderAuthOpts{Token: "test-token"}
257+
258+
_, _, err := provider.Resolve("p1", "", authOpts, tc.opts...)
259+
require.NoError(t, err)
260+
_, _, err = provider.ResolveGroup("g1", "", authOpts, tc.opts...)
261+
require.NoError(t, err)
262+
263+
assert.Equal(t, tc.wantParam, policyParam, "Resolve")
264+
assert.Equal(t, tc.wantParam, groupParam, "ResolveGroup")
265+
})
266+
}
267+
}
268+
217269
func TestValidateAttachmentHTTPStatusHandling(t *testing.T) {
218270
testCases := []struct {
219271
name string

0 commit comments

Comments
 (0)