Skip to content
Open
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,8 @@ hack/**/charts
benchmark/*/data.tar.gz

# JUnit file output from acceptance tests
junit-acceptance.xml
junit-acceptance.xml

# Generated TUF ConfigMaps (now created just-in-time)
hack/tuf/tuf-files-configmap.yaml
hack/tuf/tuf-root-configmap.yaml
25 changes: 23 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ test: ## Run all unit tests
ACCEPTANCE_TIMEOUT:=20m
.ONESHELL:
.SHELLFLAGS=-e -c

# Generate TUF ConfigMaps if they don't exist or source files are newer
hack/tuf/tuf-files-configmap.yaml: acceptance/wiremock/recordings/tuf/__files/* acceptance/wiremock/recordings/tuf/mappings/* acceptance/tuf/root.json hack/tuf/create-tuf-files.sh
@echo "Generating TUF ConfigMaps..."
@hack/tuf/create-tuf-files.sh

# The root ConfigMap is generated at the same time as the files ConfigMap
hack/tuf/tuf-root-configmap.yaml: hack/tuf/tuf-files-configmap.yaml

.PHONY: tuf-yaml-clean
tuf-yaml-clean: ## Remove generated TUF ConfigMap YAML files
@echo "Cleaning TUF ConfigMap YAML files..."
@rm -f hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml

.PHONY: tuf-yaml-refresh
tuf-yaml-refresh: tuf-yaml-clean tuf-yaml ## Force regeneration of TUF ConfigMap YAML files

.PHONY: tuf-yaml
tuf-yaml: hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml ## Generate TUF ConfigMap YAML files for acceptance tests

.PHONY: acceptance

acceptance: ## Run all acceptance tests
Expand All @@ -130,13 +150,14 @@ acceptance: ## Run all acceptance tests
cp -R . "$$ACCEPTANCE_WORKDIR"; \
cd "$$ACCEPTANCE_WORKDIR" && \
$(MAKE) build && \
$(MAKE) tuf-yaml && \
export GOCOVERDIR="$${ACCEPTANCE_WORKDIR}/coverage"; \
cd acceptance && go test -timeout $(ACCEPTANCE_TIMEOUT) ./... ; go tool covdata textfmt -i=$${GOCOVERDIR} -o="$(ROOT_DIR)/coverage-acceptance.out"

# Add @focus above the feature you're hacking on to use this
# (Mainly for use with the feature-% target below)
.PHONY: focus-acceptance
focus-acceptance: build ## Run acceptance tests with @focus tag
focus-acceptance: build tuf-yaml ## Run acceptance tests with @focus tag
@cd acceptance && go test . -args -tags=@focus

# Uses sed hackery to insert a @focus tag and then remove it afterwards.
Expand All @@ -157,7 +178,7 @@ feature_%: ## Run acceptance tests for a single feature file, e.g. make feature_
fi

# (Replace spaces with underscores in the scenario name.)
scenario_%: build ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy
scenario_%: build tuf-yaml ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy
@cd acceptance && go test -test.run 'TestFeatures/$*'

benchmark/%/data.tar.gz:
Expand Down
10 changes: 9 additions & 1 deletion acceptance/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,11 @@ func createAndPushKeylessImage(ctx context.Context, imageName string) (context.C
continue
}

// Skip empty signatures (DSSE envelopes have empty signature annotations)
if signature == "" {
continue
}

sig := Signature{
Signature: signature,
}
Expand Down Expand Up @@ -995,7 +1000,10 @@ func RawAttestationSignaturesFrom(ctx context.Context) map[string]string {

ret := map[string]string{}
for ref, signature := range state.AttestationSignatures {
ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature
// Only add the variable if the signature is not empty to avoid polluting snapshots
if signature.Signature != "" {
ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature
}
}

return ret
Expand Down
2 changes: 1 addition & 1 deletion acceptance/kubernetes/kind/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func Start(givenCtx context.Context) (ctx context.Context, kCluster types.Cluste

var port int
if port, err = freeport.GetFreePort(); err != nil {
logger.Errorf("Unable to determine a free port: %v", err)
logger.Errorf("Unable to determine a free port for registry: %v", err)
return
} else {
kCluster.registryPort = int32(port) //nolint:gosec // G115 - ports shouldn't be larger than int32
Expand Down
148 changes: 148 additions & 0 deletions acceptance/kubernetes/kind/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ import (
pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
tekton "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

"github.com/conforma/cli/acceptance/crypto"
"github.com/conforma/cli/acceptance/kubernetes/types"
"github.com/conforma/cli/acceptance/kustomize"
"github.com/conforma/cli/acceptance/rekor"
"github.com/conforma/cli/acceptance/testenv"
"github.com/conforma/cli/acceptance/wiremock"
)

// createPolicyObject creates the EnterpriseContractPolicy object with the given
Expand Down Expand Up @@ -189,6 +193,137 @@ func (k *kindCluster) CreateNamedSnapshot(ctx context.Context, name string, spec
return k.createSnapshot(ctx, snapshot)
}

// CreateConfigMap creates a ConfigMap with the given name and namespace with the provided content
// Also creates necessary RBAC permissions for cross-namespace access
func (k *kindCluster) CreateConfigMap(ctx context.Context, name, namespace, content string) error {
var data map[string]string

// Parse JSON content and extract individual fields as ConfigMap data keys
if strings.HasPrefix(strings.TrimSpace(content), "{") {
// Parse JSON content
var jsonData map[string]interface{}
if err := json.Unmarshal([]byte(content), &jsonData); err != nil {
return fmt.Errorf("failed to parse JSON content: %w", err)
}

// Convert to string map for ConfigMap data
data = make(map[string]string)
for key, value := range jsonData {
if value != nil {
data[key] = fmt.Sprintf("%v", value)
}
}
} else {
// For non-JSON content, store as-is under a single key
data = map[string]string{
"content": content,
}
}

configMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: data,
}

// Create the ConfigMap (or update if it already exists)
if _, err := k.client.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
// ConfigMap exists, so get the existing one to retrieve its ResourceVersion
existing, err := k.client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get existing ConfigMap: %w", err)
}
// Set the ResourceVersion from the existing ConfigMap
configMap.ResourceVersion = existing.ResourceVersion
// Now update with the proper ResourceVersion
if _, err := k.client.CoreV1().ConfigMaps(namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("failed to update existing ConfigMap: %w", err)
}
} else {
return err
}
}

// Create RBAC permissions for cross-namespace ConfigMap access
// This allows any service account to read ConfigMaps from any namespace
if err := k.ensureConfigMapRBAC(ctx); err != nil {
return fmt.Errorf("failed to create RBAC permissions: %w", err)
}

return nil
}

// ensureConfigMapRBAC creates necessary RBAC permissions for ConfigMap access across namespaces
func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context) error {
// Create ClusterRole for ConfigMap reading (idempotent)
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "acceptance-configmap-reader",
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"configmaps"},
Verbs: []string{"get", "list"},
},
},
}

if _, err := k.client.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}); err != nil {
// Ignore error if ClusterRole already exists
if !strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("failed to create ClusterRole: %w", err)
}
}

// Create ClusterRoleBinding for all service accounts (idempotent)
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "acceptance-configmap-reader-binding",
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: "acceptance-configmap-reader",
},
Subjects: []rbacv1.Subject{
{
Kind: "Group",
Name: "system:serviceaccounts",
APIGroup: "rbac.authorization.k8s.io",
},
},
}
Comment on lines +282 to +299
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

RBAC binding is too broad (system:serviceaccounts cluster-wide).

Line 295 binds ConfigMap read access to all service accounts in the cluster. For acceptance tests this should be scoped to the specific task service account/namespace only.

🔒 Scope RBAC to the test service account instead of all SAs
-func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context) error {
+func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context, configMapNamespace string) error {
+  t := testenv.FetchState[testState](ctx)
@@
-  clusterRoleBinding := &rbacv1.ClusterRoleBinding{
+  roleBinding := &rbacv1.RoleBinding{
     ObjectMeta: metav1.ObjectMeta{
-      Name: "acceptance-configmap-reader-binding",
+      Name:      fmt.Sprintf("acceptance-configmap-reader-%s", t.namespace),
+      Namespace: configMapNamespace,
     },
     RoleRef: rbacv1.RoleRef{
       APIGroup: "rbac.authorization.k8s.io",
       Kind:     "ClusterRole",
       Name:     "acceptance-configmap-reader",
     },
     Subjects: []rbacv1.Subject{
       {
-        Kind:     "Group",
-        Name:     "system:serviceaccounts",
-        APIGroup: "rbac.authorization.k8s.io",
+        Kind:      "ServiceAccount",
+        Name:      "default",
+        Namespace: t.namespace,
       },
     },
   }
-  if _, err := k.client.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}); err != nil {
+  if _, err := k.client.RbacV1().RoleBindings(configMapNamespace).Create(ctx, roleBinding, metav1.CreateOptions{}); err != nil {
     ...
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acceptance/kubernetes/kind/kubernetes.go` around lines 282 - 299, The
ClusterRoleBinding currently grants read access to all cluster service accounts
via the Subject Group "system:serviceaccounts"; update the ClusterRoleBinding
(variable clusterRoleBinding of type rbacv1.ClusterRoleBinding) to scope access
to the specific test service account by replacing the Subjects entry with a
Subject of Kind "ServiceAccount", Name set to the test SA (e.g. the task service
account name used in tests), and Namespace set to the test namespace; keep the
RoleRef (acceptance-configmap-reader) but ensure the binding name and Subject
Namespace match the acceptance test namespace so the permission is limited to
only that service account.


if _, err := k.client.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}); err != nil {
// Ignore error if ClusterRoleBinding already exists
if !strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("failed to create ClusterRoleBinding: %w", err)
}
}

return nil
}

// CreateNamedNamespace creates a namespace with the specified name
func (k *kindCluster) CreateNamedNamespace(ctx context.Context, name string) error {
_, err := k.client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}, metav1.CreateOptions{})

// Ignore error if namespace already exists
if err != nil && strings.Contains(err.Error(), "already exists") {
return nil
}

return err
}

// CreateNamespace creates a randomly-named namespace for the test to execute in
// and stores it in the test context
func (k *kindCluster) CreateNamespace(ctx context.Context) (context.Context, error) {
Expand Down Expand Up @@ -254,6 +389,19 @@ func stringParam(ctx context.Context, name, value string, t *testState) pipeline
vars["BUILD_SNAPSHOT_DIGEST"] = t.snapshotDigest
}

// Add TUF and certificate variables for keyless verification
// For Tekton tasks, always use the cluster-internal TUF service
vars["TUF"] = "http://tuf.tuf-service.svc.cluster.local:8080"
vars["CERT_IDENTITY"] = "https://kubernetes.io/namespaces/default/serviceaccounts/default"
vars["CERT_ISSUER"] = "https://kubernetes.default.svc.cluster.local"

// Only set REKOR variable if stub rekord was started
if wiremock.IsRunning(ctx) {
if rekorURL, err := rekor.StubRekor(ctx); err == nil {
vars["REKOR"] = rekorURL
}
}

publicKeys := crypto.PublicKeysFrom(ctx)
for name, key := range publicKeys {
vars[fmt.Sprintf("%s_PUBLIC_KEY", name)] = key
Expand Down
Loading
Loading