diff --git a/internal/cache/cr.go b/internal/cache/cr.go new file mode 100644 index 00000000..6c224379 --- /dev/null +++ b/internal/cache/cr.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package cache + +import ( + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + rbacv1 "k8s.io/api/rbac/v1" +) + +// ClusterRole represents ClusterRole cache. +type ClusterRole struct { + db *db.DB +} + +// NewClusterRole returns a new ClusterRole cache. +func NewClusterRole(db *db.DB) *ClusterRole { + return &ClusterRole{db: db} +} + +// RoleRefs computes all role external references. +func (r *ClusterRole) AggregationMatchers(refs *sync.Map) { + txn, it := r.db.MustITFor(internal.Glossary[internal.CR]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cr := o.(*rbacv1.ClusterRole) + if cr.AggregationRule != nil { + for _, lbs := range cr.AggregationRule.ClusterRoleSelectors { + for k, v := range lbs.MatchLabels { + refs.Store(k, v) + } + } + } + } +} diff --git a/internal/cache/cr_test.go b/internal/cache/cr_test.go new file mode 100644 index 00000000..83ec3411 --- /dev/null +++ b/internal/cache/cr_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package cache_test + +import ( + "sync" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestClusterRoleAggregation(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + + cr := cache.NewClusterRole(dba) + var aRefs sync.Map + cr.AggregationMatchers(&aRefs) + + value, ok := aRefs.Load("rbac.authorization.k8s.io/aggregate-to-cr4") + assert.True(t, ok) + assert.Equal(t, "true", value) +} diff --git a/internal/cache/testdata/auth/cr/1.yaml b/internal/cache/testdata/auth/cr/1.yaml new file mode 100644 index 00000000..b23a4a66 --- /dev/null +++ b/internal/cache/testdata/auth/cr/1.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + name: cr4 + aggregationRule: + clusterRoleSelectors: + - matchLabels: + rbac.authorization.k8s.io/aggregate-to-cr4: "true" + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + rbac.authorization.k8s.io/aggregate-to-cr4: "true" + name: cr5 + rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - list \ No newline at end of file diff --git a/internal/lint/cr.go b/internal/lint/cr.go index 76fafdd1..287453ce 100644 --- a/internal/lint/cr.go +++ b/internal/lint/cr.go @@ -55,17 +55,19 @@ func NewClusterRole(c *issues.Collector, db *db.DB) *ClusterRole { // Lint sanitizes the resource. func (s *ClusterRole) Lint(ctx context.Context) error { - var crRefs sync.Map + var crRefs, agRefs sync.Map crb := cache.NewClusterRoleBinding(s.db) crb.ClusterRoleRefs(&crRefs) rb := cache.NewRoleBinding(s.db) rb.RoleRefs(&crRefs) - s.checkStale(ctx, &crRefs) + cr := cache.NewClusterRole(s.db) + cr.AggregationMatchers(&agRefs) + s.checkStale(ctx, &crRefs, &agRefs) return nil } -func (s *ClusterRole) checkStale(ctx context.Context, refs *sync.Map) { +func (s *ClusterRole) checkStale(ctx context.Context, refs *sync.Map, agRefs *sync.Map) { txn, it := s.db.MustITFor(internal.Glossary[internal.CR]) defer txn.Abort() for o := it.Next(); o != nil; o = it.Next() { @@ -76,6 +78,16 @@ func (s *ClusterRole) checkStale(ctx context.Context, refs *sync.Map) { if s.system.skip(fqn) { continue } + partialRole := false + for key, value := range cr.Labels { + expectedValue, ok := agRefs.Load(key) + if ok && value == expectedValue { + partialRole = true + } + } + if partialRole { + continue + } if _, ok := refs.Load(cache.ResFqn(cache.ClusterRoleKey, fqn)); !ok { s.AddCode(ctx, 400) } diff --git a/internal/lint/cr_test.go b/internal/lint/cr_test.go index 96a0782c..f5395c96 100644 --- a/internal/lint/cr_test.go +++ b/internal/lint/cr_test.go @@ -43,3 +43,22 @@ func TestCRLint(t *testing.T) { assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) } + +func TestCRLintAggregations(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/2.yaml", internal.Glossary[internal.CR])) + + cr := NewClusterRole(test.MakeCollector(t), dba) + assert.Nil(t, cr.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/clusterroles", "clusterroles"))) + assert.Equal(t, 2, len(cr.Outcome())) + + ii := cr.Outcome()["cr4"] + assert.Equal(t, 1, len(ii)) + + ii = cr.Outcome()["cr5"] + assert.Equal(t, 0, len(ii)) +} diff --git a/internal/lint/testdata/auth/cr/2.yaml b/internal/lint/testdata/auth/cr/2.yaml new file mode 100644 index 00000000..b23a4a66 --- /dev/null +++ b/internal/lint/testdata/auth/cr/2.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + name: cr4 + aggregationRule: + clusterRoleSelectors: + - matchLabels: + rbac.authorization.k8s.io/aggregate-to-cr4: "true" + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + rbac.authorization.k8s.io/aggregate-to-cr4: "true" + name: cr5 + rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - list \ No newline at end of file diff --git a/spinach-examples/spinach_aks.yml b/spinach-examples/spinach_aks.yml index a50dca40..7dcf12cb 100644 --- a/spinach-examples/spinach_aks.yml +++ b/spinach-examples/spinach_aks.yml @@ -12,7 +12,7 @@ popeye: # Checks if mem is over allocated by more than 50% at current load. overPercUtilization: 50 - # Excludes define rules to exampt resources from sanitization + # Excludes define rules to exempt resources from sanitization excludes: global: fqns: [rx:kube-system] diff --git a/spinach-examples/spinach_eks.yml b/spinach-examples/spinach_eks.yml index fa8dbf43..99c05658 100644 --- a/spinach-examples/spinach_eks.yml +++ b/spinach-examples/spinach_eks.yml @@ -12,7 +12,7 @@ popeye: # Checks if mem is over allocated by more than 50% at current load. overPercUtilization: 50 - # Excludes define rules to exampt resources from sanitization + # Excludes define rules to exempt resources from sanitization excludes: global: fqns: [rx:^kube-system,rx:^local-path-storage]