Skip to content

Commit

Permalink
feat(policies): new policy groups schema with materials (chainloop-de…
Browse files Browse the repository at this point in the history
…v#1465)

Signed-off-by: Jose I. Paris <[email protected]>
  • Loading branch information
jiparis authored Nov 4, 2024
1 parent 16a498a commit 9f7e746
Show file tree
Hide file tree
Showing 40 changed files with 698 additions and 325 deletions.
56 changes: 55 additions & 1 deletion app/cli/internal/action/attestation_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import (
"strconv"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
clientAPI "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
"github.com/chainloop-dev/chainloop/pkg/policies"
"github.com/rs/zerolog"
)

type AttestationInitOpts struct {
Expand Down Expand Up @@ -126,6 +129,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun

action.Logger.Debug().Msg("workflow contract and metadata retrieved from the control plane")

// 3. enrich contract with group materials and policies
err = enrichContractMaterials(ctx, contractVersion.GetV1(), client, &action.Logger)
if err != nil {
return "", fmt.Errorf("failed to apply materials from policy groups: %w", err)
}

// Auto discover the runner context and enforce against the one in the contract if needed
discoveredRunner, err := crafter.DiscoverAndEnforceRunner(contractVersion.GetV1().GetRunner().GetType(), action.dryRun, action.Logger)
if err != nil {
Expand Down Expand Up @@ -164,7 +173,8 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
// NOTE: important to run this initialization here since workflowMeta is populated
// with the workflowRunId that comes from the control plane
initOpts := &crafter.InitOpts{
WfInfo: workflowMeta, SchemaV1: contractVersion.GetV1(),
WfInfo: workflowMeta,
SchemaV1: contractVersion.GetV1(),
DryRun: action.dryRun,
AttestationID: attestationID,
Runner: discoveredRunner,
Expand All @@ -186,3 +196,47 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun

return attestationID, nil
}

func enrichContractMaterials(ctx context.Context, schema *v1.CraftingSchema, client pb.AttestationServiceClient, logger *zerolog.Logger) error {
contractMaterials := schema.GetMaterials()
for _, pgAtt := range schema.GetPolicyGroups() {
group, _, err := policies.LoadPolicyGroup(ctx, pgAtt, &policies.LoadPolicyGroupOptions{
Client: client,
Logger: logger,
})
if err != nil {
// Temporarily skip if policy groups still use old schema
// TODO: remove this check in next release
logger.Warn().Msgf("policy group '%s' skipped since it's not found or it might use an old schema version", pgAtt.GetRef())
return nil
}
logger.Debug().Msgf("adding materials from policy group '%s'", group.GetMetadata().GetName())

toAdd := getGroupMaterialsToAdd(group, contractMaterials, logger)
contractMaterials = append(contractMaterials, toAdd...)
}

schema.Materials = contractMaterials

return nil
}

// merge existing materials with group ones, taking the contract's one in case of conflict
func getGroupMaterialsToAdd(group *v1.PolicyGroup, fromContract []*v1.CraftingSchema_Material, logger *zerolog.Logger) []*v1.CraftingSchema_Material {
toAdd := make([]*v1.CraftingSchema_Material, 0)
for _, groupMaterial := range group.GetSpec().GetPolicies().GetMaterials() {
// check if material already exists in the contract and skip it in that case
ignore := false
for _, mat := range fromContract {
if mat.GetName() == groupMaterial.GetName() {
logger.Warn().Msgf("material '%s' from policy group '%s' is also present in the contract and will be ignored", mat.GetName(), group.GetMetadata().GetName())
ignore = true
}
}
if !ignore {
toAdd = append(toAdd, groupMaterial)
}
}

return toAdd
}
104 changes: 104 additions & 0 deletions app/cli/internal/action/attestation_init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright 2024 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package action

import (
"context"
"slices"
"testing"

v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnrichMaterials(t *testing.T) {
cases := []struct {
name string
materials []*v1.CraftingSchema_Material
policyGroup string
expectErr bool
nMaterials int
nPolicies int
}{
{
name: "existing material",
materials: []*v1.CraftingSchema_Material{
{
Type: v1.CraftingSchema_Material_SBOM_SPDX_JSON,
Name: "sbom",
},
},
policyGroup: "file://testdata/policy_group.yaml",
nMaterials: 2,
nPolicies: 0,
},
{
name: "new materials",
materials: []*v1.CraftingSchema_Material{
{
Type: v1.CraftingSchema_Material_SBOM_CYCLONEDX_JSON,
Name: "another-sbom",
},
},
policyGroup: "file://testdata/policy_group.yaml",
nMaterials: 3,
nPolicies: 1,
},
{
name: "empty materials in schema",
materials: []*v1.CraftingSchema_Material{},
policyGroup: "file://testdata/policy_group.yaml",
nMaterials: 2,
nPolicies: 1,
},
{
name: "wrong policy group",
materials: []*v1.CraftingSchema_Material{},
policyGroup: "file://testdata/idontexist.yaml",
// TODO: Fix this condition in next release
expectErr: false,
},
}

l := zerolog.Nop()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
schema := v1.CraftingSchema{
Materials: tc.materials,
PolicyGroups: []*v1.PolicyGroupAttachment{
{
Ref: tc.policyGroup,
},
},
}
err := enrichContractMaterials(context.TODO(), &schema, nil, &l)
if tc.expectErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, schema.Materials, tc.nMaterials)
// find "sbom" material and check it has proper policies
if tc.nMaterials > 0 {
assert.True(t, slices.ContainsFunc(schema.Materials, func(m *v1.CraftingSchema_Material) bool {
return m.Name == "sbom" && len(m.Policies) == tc.nPolicies
}))
}
})
}
}
18 changes: 18 additions & 0 deletions app/cli/internal/action/testdata/policy_group.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: workflowcontract.chainloop.dev/v1
kind: PolicyGroup
metadata:
name: sbom-quality
description: This policy group applies a number of SBOM-related policies
annotations:
category: SBOM
spec:
policies:
attestation:
- ref: file://testdata/with_arguments.yaml
materials:
- name: sbom
type: SBOM_SPDX_JSON
policies:
- ref: file://testdata/multi-kind.yaml
- name: container
type: CONTAINER_IMAGE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9f7e746

Please sign in to comment.