Skip to content

Commit 00707ba

Browse files
Support policy type manifest (#41)
Add support for Policy type manifests in policy generator. Signed-off-by: Chunxi Luo <[email protected]>
1 parent 7f358d4 commit 00707ba

File tree

7 files changed

+314
-4
lines changed

7 files changed

+314
-4
lines changed

docs/policygenerator-reference.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ policies:
9090
# kustomization.yaml file. This path cannot be in a directory outside of the directory with
9191
# the kustomization.yaml file. Subdirectories within the directory with kustomization.yaml
9292
# file are allowed.
93+
# Supported manifests:
94+
# 1) Non-root policy type manifests such as IamPolicy, CertificatePolicy, and ConfigurationPolicy
95+
# that have a "Policy" suffix. These are not modified except for patches and are directly added
96+
# as a Policy's policy-templates entry.
97+
# 2) For everything else, ConfigurationPolicy objects are generated to wrap these manifests.
98+
# The resulting ConfigurationPolicy is added as a Policy's policy-templates entry.
9399
- path: ""
94100
# Optional. (See policy[0].complianceType for description.)
95101
complianceType: "musthave"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: policy.open-cluster-management.io/v1
2+
kind: IamPolicy
3+
metadata:
4+
name: policy-limitclusteradmin-example
5+
spec:
6+
severity: medium
7+
namespaceSelector:
8+
include: ["*"]
9+
exclude: ["kube-*", "openshift-*"]
10+
remediationAction: inform
11+
maxClusterRoleBindingUsers: 5

examples/policyGenerator.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ policies:
5757
- path: input-gatekeeper/
5858
policySets:
5959
- policyset-gatekeeper
60+
- name: policy-limitclusteradmin
61+
categories:
62+
- AC Access Control
63+
controls:
64+
- AC-3 Access Enforcement
65+
standards:
66+
- NIST SP 800-53
67+
manifests:
68+
- path: input-policy-type/iam.yaml
69+
policySets:
70+
- policyset-iam
6071
policySets:
6172
- name: policyset-kyverno
6273
description: this is a kyverno policy set.
@@ -67,4 +78,6 @@ policySets:
6778
- name: policyset-gatekeeper
6879
description: this is a gatekeeper policy set.
6980
placement:
70-
placementRulePath: input/placementrule.yaml
81+
placementRulePath: input/placementrule.yaml
82+
- name: policyset-iam
83+
description: this is a iam policy set.

internal/plugin_config_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ data:
2828
}
2929
}
3030

31+
func createIamPolicyManifest(t *testing.T, tmpDir, filename string) {
32+
t.Helper()
33+
manifestsPath := path.Join(tmpDir, filename)
34+
yamlContent := `
35+
apiVersion: policy.open-cluster-management.io/v1
36+
kind: IamPolicy
37+
metadata:
38+
name: policy-limitclusteradmin-example
39+
spec:
40+
severity: medium
41+
namespaceSelector:
42+
include: ["*"]
43+
exclude: ["kube-*", "openshift-*"]
44+
remediationAction: enforce
45+
maxClusterRoleBindingUsers: 5
46+
`
47+
err := ioutil.WriteFile(manifestsPath, []byte(yamlContent), 0o666)
48+
if err != nil {
49+
t.Fatalf("Failed to write %s", manifestsPath)
50+
}
51+
}
52+
3153
func TestConfig(t *testing.T) {
3254
t.Parallel()
3355
tmpDir := t.TempDir()

internal/plugin_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,67 @@ spec:
420420
assertEqual(t, output, expected)
421421
}
422422

423+
func TestCreatePolicyFromIamPolicyTypeManifest(t *testing.T) {
424+
t.Parallel()
425+
tmpDir := t.TempDir()
426+
createIamPolicyManifest(t, tmpDir, "iamKindManifestPluginTest.yaml")
427+
p := Plugin{}
428+
p.PolicyDefaults.Namespace = "Iam-policies"
429+
policyConf := types.PolicyConfig{
430+
Categories: []string{"AC Access Control"},
431+
Controls: []string{"AC-3 Access Enforcement"},
432+
Standards: []string{"NIST SP 800-53"},
433+
Name: "policy-limitclusteradmin",
434+
Manifests: []types.Manifest{
435+
{Path: path.Join(tmpDir, "iamKindManifestPluginTest.yaml")},
436+
},
437+
}
438+
p.Policies = append(p.Policies, policyConf)
439+
p.applyDefaults(map[string]interface{}{})
440+
441+
err := p.createPolicy(&p.Policies[0])
442+
if err != nil {
443+
t.Fatal(err.Error())
444+
}
445+
446+
output := p.outputBuffer.String()
447+
// expected Iam policy generated from
448+
// non-root IAM policy type manifest
449+
// in createIamPolicyTypeConfigMap()
450+
expected := `
451+
---
452+
apiVersion: policy.open-cluster-management.io/v1
453+
kind: Policy
454+
metadata:
455+
annotations:
456+
policy.open-cluster-management.io/categories: AC Access Control
457+
policy.open-cluster-management.io/controls: AC-3 Access Enforcement
458+
policy.open-cluster-management.io/standards: NIST SP 800-53
459+
name: policy-limitclusteradmin
460+
namespace: Iam-policies
461+
spec:
462+
disabled: false
463+
policy-templates:
464+
- objectDefinition:
465+
apiVersion: policy.open-cluster-management.io/v1
466+
kind: IamPolicy
467+
metadata:
468+
name: policy-limitclusteradmin-example
469+
spec:
470+
maxClusterRoleBindingUsers: 5
471+
namespaceSelector:
472+
exclude:
473+
- kube-*
474+
- openshift-*
475+
include:
476+
- '*'
477+
remediationAction: enforce
478+
severity: medium
479+
`
480+
expected = strings.TrimPrefix(expected, "\n")
481+
assertEqual(t, output, expected)
482+
}
483+
423484
func TestCreatePolicyDir(t *testing.T) {
424485
t.Parallel()
425486
tmpDir := t.TempDir()
@@ -517,6 +578,43 @@ func TestCreatePolicyInvalidYAML(t *testing.T) {
517578
assertEqual(t, err.Error(), expected)
518579
}
519580

581+
func TestCreatePolicyInvalidAPIOrKind(t *testing.T) {
582+
t.Parallel()
583+
tmpDir := t.TempDir()
584+
manifestPath := path.Join(tmpDir, "invalidAPIOrKind.yaml")
585+
yamlContent := `
586+
apiVersion: policy.open-cluster-management.io/v1
587+
kind:
588+
- IamPolicy
589+
- CertificatePolicy
590+
metadata:
591+
name: policy-limitclusteradmin-example
592+
`
593+
err := ioutil.WriteFile(manifestPath, []byte(yamlContent), 0o666)
594+
if err != nil {
595+
t.Fatalf("Failed to create %s: %v", manifestPath, err)
596+
}
597+
598+
p := Plugin{}
599+
p.PolicyDefaults.Namespace = "my-policies"
600+
policyConf := types.PolicyConfig{
601+
Name: "policy-limitclusteradmin",
602+
Manifests: []types.Manifest{{Path: manifestPath}},
603+
}
604+
p.Policies = append(p.Policies, policyConf)
605+
p.applyDefaults(map[string]interface{}{})
606+
607+
err = p.createPolicy(&p.Policies[0])
608+
if err == nil {
609+
t.Fatal("Expected an error but did not get one")
610+
}
611+
612+
expected := fmt.Sprintf(
613+
"invalid non-string kind format in manifest path: %s", manifestPath,
614+
)
615+
assertEqual(t, err.Error(), expected)
616+
}
617+
520618
func TestCreatePlacementDefault(t *testing.T) {
521619
t.Parallel()
522620
p := Plugin{}

internal/utils.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/stolostron/policy-generator-plugin/internal/expanders"
1616
"github.com/stolostron/policy-generator-plugin/internal/types"
1717
"gopkg.in/yaml.v3"
18+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1819
)
1920

2021
// getManifests will get all of the manifest files associated with the input policy configuration
@@ -139,12 +140,28 @@ func getPolicyTemplates(policyConf *types.PolicyConfig) ([]map[string]map[string
139140
for i, manifestGroup := range manifestGroups {
140141
complianceType := policyConf.Manifests[i].ComplianceType
141142
for _, manifest := range manifestGroup {
143+
isPolicyTypeManifest, err := isPolicyTypeManifest(manifest)
144+
if err != nil {
145+
return nil, fmt.Errorf(
146+
"%w in manifest path: %s",
147+
err,
148+
policyConf.Manifests[i].Path,
149+
)
150+
}
151+
152+
if isPolicyTypeManifest {
153+
policyTemplate := map[string]map[string]interface{}{"objectDefinition": manifest}
154+
policyTemplates = append(policyTemplates, policyTemplate)
155+
156+
continue
157+
}
158+
142159
objTemplate := map[string]interface{}{
143160
"complianceType": complianceType,
144161
"objectDefinition": manifest,
145162
}
146163
if policyConf.ConsolidateManifests {
147-
// put all objTemplate with manifest into single consolidated objectTemplates object
164+
// put all objTemplate with manifest into single consolidated objectTemplates
148165
objectTemplates = append(objectTemplates, objTemplate)
149166
} else {
150167
// casting each objTemplate with manifest to objectTemplates type
@@ -167,8 +184,9 @@ func getPolicyTemplates(policyConf *types.PolicyConfig) ([]map[string]map[string
167184
)
168185
}
169186

170-
// just build one policyTemplate by using the above consolidated objectTemplates
171-
if policyConf.ConsolidateManifests {
187+
// just build one policyTemplate by using the above non-empty consolidated objectTemplates
188+
// ConsolidateManifests = true or there is non-policy-type manifest
189+
if policyConf.ConsolidateManifests && len(objectTemplates) > 0 {
172190
policyTemplate := buildPolicyTemplate(policyConf, 1, &objectTemplates, &policyConf.EvaluationInterval)
173191
setNamespaceSelector(policyConf, policyTemplate)
174192
policyTemplates = append(policyTemplates, *policyTemplate)
@@ -183,6 +201,24 @@ func getPolicyTemplates(policyConf *types.PolicyConfig) ([]map[string]map[string
183201
return policyTemplates, nil
184202
}
185203

204+
// isPolicyTypeManifest determines if the manifest is a non-root policy manifest
205+
// by checking apiVersion and kind fields.
206+
// Return error when apiVersion and kind fields aren't string.
207+
func isPolicyTypeManifest(manifest map[string]interface{}) (bool, error) {
208+
apiVersion, _, isAPIStr := unstructured.NestedString(manifest, "apiVersion")
209+
kind, _, isKindStr := unstructured.NestedString(manifest, "kind")
210+
if isAPIStr != nil {
211+
return false, fmt.Errorf("invalid non-string apiVersion format")
212+
} else if isKindStr != nil {
213+
return false, fmt.Errorf("invalid non-string kind format")
214+
} else if strings.HasPrefix(apiVersion, "policy.open-cluster-management.io") &&
215+
kind != "Policy" && strings.HasSuffix(kind, "Policy") {
216+
return true, nil // non-root policy-type manifest contains policy api
217+
}
218+
219+
return false, nil
220+
}
221+
186222
// setNamespaceSelector sets the namespace selector, if set, on the input policy template.
187223
func setNamespaceSelector(policyConf *types.PolicyConfig, policyTemplate *map[string]map[string]interface{}) {
188224
if policyConf.NamespaceSelector.Exclude != nil || policyConf.NamespaceSelector.Include != nil {

internal/utils_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,130 @@ data:
233233
}
234234
}
235235

236+
func TestIsPolicyTypeManifest(t *testing.T) {
237+
t.Parallel()
238+
239+
invalidAPI := []string{
240+
"policy.open-cluster-management.io/v1",
241+
"apps.open-cluster-management.io/v1",
242+
}
243+
244+
invalidKind := []string{
245+
"CertificatePolicy",
246+
"IamPolicy",
247+
}
248+
249+
tests := []struct {
250+
apiVersion string
251+
kind string
252+
invalidAPI []string
253+
invalidKind []string
254+
expectedFlag bool
255+
expectedErrMsg string
256+
}{
257+
{"policy.open-cluster-management.io/v1", "IamPolicy", nil, nil, true, ""},
258+
{"policy.open-cluster-management.io/v1", "CertificatePolicy", nil, nil, true, ""},
259+
{"policy.open-cluster-management.io/v1", "ConfigurationPolicy", nil, nil, true, ""},
260+
{"policy.open-cluster-management.io/v1", "Policy", nil, nil, false, ""},
261+
{"apps.open-cluster-management.io/v1", "PlacementRule", nil, nil, false, ""},
262+
{"", "", nil, nil, false, ""},
263+
{"", "IamPolicy", invalidAPI, nil, false, "invalid non-string apiVersion format"},
264+
{"policy.open-cluster-management.io/v1", "", nil, invalidKind, false, "invalid non-string kind format"},
265+
}
266+
267+
for _, test := range tests {
268+
test := test
269+
t.Run(
270+
fmt.Sprintf("apiVersion=%s, kind=%s", test.apiVersion, test.kind),
271+
func(t *testing.T) {
272+
t.Parallel()
273+
manifest := map[string]interface{}{}
274+
275+
if test.invalidAPI == nil {
276+
manifest["apiVersion"] = test.apiVersion
277+
} else {
278+
manifest["apiVersion"] = test.invalidAPI
279+
}
280+
281+
if test.invalidKind == nil {
282+
manifest["kind"] = test.kind
283+
} else {
284+
manifest["kind"] = test.invalidKind
285+
}
286+
287+
isPolicyType, err := isPolicyTypeManifest(manifest)
288+
assertEqual(t, isPolicyType, test.expectedFlag)
289+
290+
if test.expectedErrMsg == "" {
291+
assertEqual(t, err, nil)
292+
} else {
293+
assertEqual(t, err.Error(), test.expectedErrMsg)
294+
}
295+
},
296+
)
297+
}
298+
}
299+
300+
func TestGetPolicyTemplateFromPolicyTypeManifest(t *testing.T) {
301+
t.Parallel()
302+
tmpDir := t.TempDir()
303+
manifestFiles := []types.Manifest{}
304+
createIamPolicyManifest(t, tmpDir, "iamKindManifest.yaml")
305+
// Test manifest is non-root IAM policy type.
306+
IamManifestPath := path.Join(tmpDir, "iamKindManifest.yaml")
307+
308+
manifestFiles = append(
309+
manifestFiles, types.Manifest{Path: IamManifestPath},
310+
)
311+
312+
// Test both passing in individual files and a flat directory.
313+
tests := []struct {
314+
Manifests []types.Manifest
315+
}{
316+
{Manifests: manifestFiles},
317+
{
318+
Manifests: []types.Manifest{{Path: tmpDir}},
319+
},
320+
}
321+
322+
for _, test := range tests {
323+
policyConf := types.PolicyConfig{
324+
Manifests: test.Manifests,
325+
Name: "policy-limitclusteradmin",
326+
RemediationAction: "inform",
327+
Severity: "low",
328+
}
329+
330+
policyTemplates, err := getPolicyTemplates(&policyConf)
331+
if err != nil {
332+
t.Fatalf("Failed to get the policy templates: %v", err)
333+
}
334+
assertEqual(t, len(policyTemplates), 1)
335+
336+
IamPolicyTemplate := policyTemplates[0]
337+
IamObjdef := IamPolicyTemplate["objectDefinition"]
338+
assertEqual(t, IamObjdef["apiVersion"], "policy.open-cluster-management.io/v1")
339+
// kind will not be overridden by "ConfigurationPolicy".
340+
assertEqual(t, IamObjdef["kind"], "IamPolicy")
341+
assertEqual(t, IamObjdef["metadata"].(map[string]interface{})["name"], "policy-limitclusteradmin-example")
342+
IamSpec, ok := IamObjdef["spec"].(map[string]interface{})
343+
if !ok {
344+
t.Fatal("The spec field is an invalid format")
345+
}
346+
// remediationAction will not be overridden by policyConf.
347+
assertEqual(t, IamSpec["remediationAction"], "enforce")
348+
// severity will not be overridden by policyConf.
349+
assertEqual(t, IamSpec["severity"], "medium")
350+
assertEqual(t, IamSpec["maxClusterRoleBindingUsers"], 5)
351+
namespaceSelector, ok := IamSpec["namespaceSelector"].(map[string]interface{})
352+
if !ok {
353+
t.Fatal("The namespaceSelector field is an invalid format")
354+
}
355+
assertReflectEqual(t, namespaceSelector["include"], []interface{}{"*"})
356+
assertReflectEqual(t, namespaceSelector["exclude"], []interface{}{"kube-*", "openshift-*"})
357+
}
358+
}
359+
236360
func TestGetPolicyTemplatePatches(t *testing.T) {
237361
t.Parallel()
238362
tmpDir := t.TempDir()

0 commit comments

Comments
 (0)