Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions app/controlplane/pkg/biz/workflowcontract.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,9 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicy(ctx context.Context, at
return nil, err
}

remotePolicy, err := uc.GetPolicy(ctx, pr.Provider, pr.Name, pr.OrgName, "", token)
// The control plane is a non-CLI caller, so resolve the true latest revision
// and skip CLI-version compatibility gating.
remotePolicy, err := uc.GetPolicy(ctx, pr.Provider, pr.Name, pr.OrgName, "", token, policies.WithIgnoreCLICompatibility())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -637,7 +639,9 @@ func (uc *WorkflowContractUseCase) findAndValidatePolicyGroup(ctx context.Contex
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)
// The control plane is a non-CLI caller, so resolve the true latest revision
// and skip CLI-version compatibility gating.
remoteGroup, err := uc.GetPolicyGroup(ctx, pr.Provider, pr.Name, pr.OrgName, "", token, policies.WithIgnoreCLICompatibility())
if err != nil {
return nil, NewErrValidation(fmt.Errorf("failed to get policy group: %w", err))
}
Expand Down Expand Up @@ -779,14 +783,15 @@ func providerAuthOpts(ctx context.Context, token, currentOrgName string) policie
}
}

// GetPolicy retrieves a policy from a policy provider
func (uc *WorkflowContractUseCase) GetPolicy(ctx context.Context, providerName, policyName, policyOrgName, currentOrgName, token string) (*RemotePolicy, error) {
// GetPolicy retrieves a policy from a policy provider.
// See policies.WithIgnoreCLICompatibility for the available resolution options.
func (uc *WorkflowContractUseCase) GetPolicy(ctx context.Context, providerName, policyName, policyOrgName, currentOrgName, token string, opts ...policies.ResolveOption) (*RemotePolicy, error) {
provider, err := uc.findProvider(providerName)
if err != nil {
return nil, err
}

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

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

group, ref, err := provider.ResolveGroup(groupName, groupOrgName, providerAuthOpts(ctx, token, currentOrgName))
group, ref, err := provider.ResolveGroup(groupName, groupOrgName, providerAuthOpts(ctx, token, currentOrgName), opts...)
if err != nil {
if errors.Is(err, policies.ErrNotFound) {
return nil, NewErrNotFound(fmt.Sprintf("policy group %q", groupName))
Expand Down
61 changes: 51 additions & 10 deletions app/controlplane/pkg/policies/policyprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ const (
validateAction = "validate"
groupsEndpoint = "groups"

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

// PolicyProvider represents an external policy provider
Expand Down Expand Up @@ -88,8 +93,37 @@ var (
ErrUnauthorized = fmt.Errorf("unauthorized request to policy provider")
)

// Resolve calls the remote provider for retrieving a policy
func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts ProviderAuthOpts) (*schemaapi.Policy, *PolicyReference, error) {
// ResolveOption configures a policy or policy-group resolution request.
type ResolveOption func(*resolveOptions)

type resolveOptions struct {
ignoreCLICompatibility bool
}

func newResolveOptions(opts []ResolveOption) resolveOptions {
var o resolveOptions
for _, opt := range opts {
if opt != nil {
opt(&o)
}
}
return o
}

// WithIgnoreCLICompatibility makes the provider skip CLI-version compatibility
// resolution and return the true latest revision instead of the latest revision
// compatible with the requesting CLI version. It is meant for non-CLI callers
// such as the web UI or the control plane. On the wire this sets the
// include_all_versions query parameter.
func WithIgnoreCLICompatibility() ResolveOption {
return func(o *resolveOptions) {
o.ignoreCLICompatibility = true
}
}

// Resolve calls the remote provider for retrieving a policy.
// See WithIgnoreCLICompatibility for the available resolution options.
func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts ProviderAuthOpts, opts ...ResolveOption) (*schemaapi.Policy, *PolicyReference, error) {
if policyName == "" || authOpts.Token == "" {
return nil, nil, fmt.Errorf("both policyname and auth opts are mandatory")
}
Expand All @@ -108,7 +142,7 @@ func (p *PolicyProvider) Resolve(policyName, policyOrgName string, authOpts Prov
}
// we want to override the orgName with the one in the response
// since we might have resolved it implicitly
providerDigest, orgName, err := p.queryProvider(url, digest, policyOrgName, authOpts, &policy)
providerDigest, orgName, err := p.queryProvider(url, digest, policyOrgName, authOpts, &policy, opts...)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve policy: %w", err)
}
Expand Down Expand Up @@ -189,8 +223,9 @@ func (p *PolicyProvider) ValidateAttachment(att *schemaapi.PolicyAttachment, aut
return nil
}

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

// returns digest, orgname, error
func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, authOpts ProviderAuthOpts, out proto.Message) (string, string, error) {
func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, authOpts ProviderAuthOpts, out proto.Message, opts ...ResolveOption) (string, string, error) {
options := newResolveOptions(opts)

query := url.Query()
if digest != "" {
query.Set(digestParam, digest)
Expand All @@ -228,6 +265,10 @@ func (p *PolicyProvider) queryProvider(url *url.URL, digest, orgName string, aut
query.Set(orgNameParam, orgName)
}

if options.ignoreCLICompatibility {
query.Set(includeAllVersionsParam, "true")
}

url.RawQuery = query.Encode()

req, err := http.NewRequest("GET", url.String(), nil)
Expand Down
52 changes: 52 additions & 0 deletions app/controlplane/pkg/policies/policyprovider_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,58 @@ func TestProviderForwardsCLIVersionHeader(t *testing.T) {
}
}

func TestResolveForwardsIgnoreCLICompatibility(t *testing.T) {
testCases := []struct {
name string
opts []ResolveOption
wantParam string
}{
{
name: "include_all_versions set when ignoring CLI compatibility",
opts: []ResolveOption{WithIgnoreCLICompatibility()},
wantParam: "true",
},
{
name: "param omitted when no options passed",
opts: nil,
wantParam: "",
},
{
name: "nil option is ignored without panicking",
opts: []ResolveOption{nil},
wantParam: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var policyParam, groupParam string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got := r.URL.Query().Get("include_all_versions")
if strings.Contains(r.URL.Path, "/groups/") {
groupParam = got
} else {
policyParam = got
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()

provider := &PolicyProvider{name: "test", url: server.URL}
authOpts := ProviderAuthOpts{Token: "test-token"}

_, _, err := provider.Resolve("p1", "", authOpts, tc.opts...)
require.NoError(t, err)
_, _, err = provider.ResolveGroup("g1", "", authOpts, tc.opts...)
require.NoError(t, err)

assert.Equal(t, tc.wantParam, policyParam, "Resolve")
assert.Equal(t, tc.wantParam, groupParam, "ResolveGroup")
})
}
}

func TestValidateAttachmentHTTPStatusHandling(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading