Skip to content

Commit 1ed21c4

Browse files
Merge pull request #845 from tkashem/audit-config
Audit policy config observer
2 parents bb5c6a5 + 4c05a68 commit 1ed21c4

17 files changed

Lines changed: 818 additions & 16 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ require (
3636
github.com/opencontainers/go-digest v1.0.0-rc1
3737
github.com/opencontainers/image-spec v1.0.1 // indirect
3838
github.com/opencontainers/runc v0.0.0-20191031171055-b133feaeeb2e // indirect
39-
github.com/openshift/api v0.0.0-20200722170803-0ba2c3658da6
39+
github.com/openshift/api v0.0.0-20200723134351-89de68875e7c
4040
github.com/openshift/build-machinery-go v0.0.0-20200713135615-1f43d26dccc7
4141
github.com/openshift/client-go v0.0.0-20200722173614-5a1b0aaeff15
4242
github.com/pkg/errors v0.9.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ github.com/opencontainers/runc v0.0.0-20191031171055-b133feaeeb2e h1:NKMVwQeEqNO
396396
github.com/opencontainers/runc v0.0.0-20191031171055-b133feaeeb2e/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
397397
github.com/openshift/api v0.0.0-20200722170803-0ba2c3658da6 h1:h2zOwAA/Zg7mc9d16q6W7/mcJppctFyqvHeE6vz1Qys=
398398
github.com/openshift/api v0.0.0-20200722170803-0ba2c3658da6/go.mod h1:IXsT3F4NjLtRzfnQvwU+g/oPWpoNsVV5vd5aaOMO8eU=
399+
github.com/openshift/api v0.0.0-20200723134351-89de68875e7c h1:qsj/GaQ1sdT584yIcGmqqRpR5xtX5jTw5Gis3/09YI4=
400+
github.com/openshift/api v0.0.0-20200723134351-89de68875e7c/go.mod h1:IXsT3F4NjLtRzfnQvwU+g/oPWpoNsVV5vd5aaOMO8eU=
399401
github.com/openshift/build-machinery-go v0.0.0-20200713135615-1f43d26dccc7 h1:iP7TOaN+tEVNUQ0ODEbN1ukjLz918lsIt7Czf8giWlM=
400402
github.com/openshift/build-machinery-go v0.0.0-20200713135615-1f43d26dccc7/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE=
401403
github.com/openshift/client-go v0.0.0-20200722173614-5a1b0aaeff15 h1:b2QkHrmaYtY6kzy2VrYLc+KBmCuTpJjgvBahPqpt6V0=
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package apiserver
2+
3+
import (
4+
"fmt"
5+
6+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
7+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8+
"k8s.io/klog/v2"
9+
10+
"github.com/openshift/library-go/pkg/operator/configobserver"
11+
"github.com/openshift/library-go/pkg/operator/events"
12+
)
13+
14+
// AuditPolicyPathGetterFunc allows the observer to be agnostic of the source of audit profile(s).
15+
// The function returns the path to the audit policy file (associated with the
16+
// given profile) in the static manifest folder.
17+
type AuditPolicyPathGetterFunc func(profile string) (string, error)
18+
19+
// NewAuditObserver returns an ObserveConfigFunc that observes the audit field of the APIServer resource
20+
// and sets the apiServerArguments:audit-policy-file field for the apiserver appropriately.
21+
func NewAuditObserver(pathGetter AuditPolicyPathGetterFunc) configobserver.ObserveConfigFunc {
22+
var (
23+
apiServerArgumentsAuditPath = []string{"apiServerArguments", "audit-policy-file"}
24+
)
25+
26+
return func(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (observed map[string]interface{}, _ []error) {
27+
defer func() {
28+
observed = configobserver.Pruned(observed, apiServerArgumentsAuditPath)
29+
}()
30+
31+
errs := []error{}
32+
33+
// if the function encounters an error it returns existing/current config, which means that
34+
// some other entity (default config in bindata ) must ensure to default the configuration.
35+
// otherwise, the apiserver won't have a path to audit policy file and it will fail to start.
36+
listers := genericListers.(APIServerLister)
37+
apiServer, err := listers.APIServerLister().Get("cluster")
38+
if err != nil {
39+
if k8serrors.IsNotFound(err) {
40+
klog.Warningf("apiserver.config.openshift.io/cluster: not found")
41+
42+
return existingConfig, errs
43+
}
44+
45+
return existingConfig, append(errs, err)
46+
}
47+
48+
desiredProfile := string(apiServer.Spec.Audit.Profile)
49+
if len(desiredProfile) == 0 {
50+
// The specified Profile is empty, so let the defaulting layer choose a default for us.
51+
return map[string]interface{}{}, errs
52+
}
53+
54+
desiredAuditPolicyPath, err := pathGetter(desiredProfile)
55+
if err != nil {
56+
return existingConfig, append(errs, fmt.Errorf("audit profile is not valid name=%s", desiredProfile))
57+
}
58+
59+
currentAuditPolicyPath, err := getCurrentPolicyPath(existingConfig, apiServerArgumentsAuditPath...)
60+
if err != nil {
61+
return existingConfig, append(errs, fmt.Errorf("audit profile is not valid name=%s", desiredProfile))
62+
}
63+
if desiredAuditPolicyPath == currentAuditPolicyPath {
64+
return existingConfig, errs
65+
}
66+
67+
// we have a change of audit policy here!
68+
observedConfig := map[string]interface{}{}
69+
if err := unstructured.SetNestedStringSlice(observedConfig, []string{desiredAuditPolicyPath}, apiServerArgumentsAuditPath...); err != nil {
70+
return existingConfig, append(errs, fmt.Errorf("failed to set desired audit profile in observed config name=%s", desiredProfile))
71+
}
72+
73+
recorder.Eventf("ObserveAPIServerArgumentsAudit", "audit policy has been set to profile=%s", desiredProfile)
74+
return observedConfig, errs
75+
}
76+
}
77+
78+
func getCurrentPolicyPath(existing map[string]interface{}, fields ...string) (string, error) {
79+
current, _, err := unstructured.NestedStringSlice(existing, fields...)
80+
if err != nil {
81+
return "", err
82+
}
83+
if len(current) == 0 {
84+
return "", nil
85+
}
86+
87+
return current[0], nil
88+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package apiserver
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
"k8s.io/client-go/tools/cache"
11+
12+
configv1 "github.com/openshift/api/config/v1"
13+
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
14+
"github.com/openshift/library-go/pkg/operator/events"
15+
)
16+
17+
var (
18+
auditPolicyFilePath = []string{"apiServerArguments", "audit-policy-file"}
19+
)
20+
21+
func TestAuditObserver(t *testing.T) {
22+
tests := []struct {
23+
name string
24+
existingConfig map[string]interface{}
25+
desiredProfile *string
26+
expectedPath string
27+
errExpected bool
28+
}{
29+
{
30+
name: "WithCurrentAndDesiredBothEmpty",
31+
existingConfig: map[string]interface{}{},
32+
desiredProfile: stringPointer(""),
33+
expectedPath: "",
34+
},
35+
{
36+
name: "WithCurrentSetAndDesiredEmpty",
37+
existingConfig: map[string]interface{}{
38+
"apiServerArguments": map[string]interface{}{
39+
"audit-policy-file": []interface{}{
40+
path("AllRequestBodies"),
41+
},
42+
},
43+
},
44+
desiredProfile: stringPointer(""),
45+
expectedPath: "",
46+
},
47+
{
48+
name: "WithCurrentEmpty",
49+
existingConfig: map[string]interface{}{},
50+
desiredProfile: stringPointer("WriteRequestBodies"),
51+
expectedPath: path("WriteRequestBodies"),
52+
},
53+
{
54+
name: "WithCurrentAndDesiredAtDifferentValues",
55+
existingConfig: map[string]interface{}{
56+
"apiServerArguments": map[string]interface{}{
57+
"audit-policy-file": []interface{}{
58+
path("AllRequestBodies"),
59+
},
60+
},
61+
},
62+
desiredProfile: stringPointer("WriteRequestBodies"),
63+
expectedPath: path("WriteRequestBodies"),
64+
},
65+
{
66+
// we expect the function to return just the keys it is responsible for.
67+
name: "WithOtherKeysDropped",
68+
existingConfig: map[string]interface{}{
69+
"apiServerArguments": map[string]interface{}{
70+
"audit-policy-file": []interface{}{
71+
path("AllRequestBodies"),
72+
},
73+
},
74+
"foo": []interface{}{
75+
"should not be returned",
76+
},
77+
},
78+
desiredProfile: stringPointer("WriteRequestBodies"),
79+
expectedPath: path("WriteRequestBodies"),
80+
},
81+
{
82+
// if the user specifies an invalid audit profile we expect the current config to be set.
83+
name: "WithCurrentSetAndDesiredInvalid",
84+
existingConfig: map[string]interface{}{
85+
"apiServerArguments": map[string]interface{}{
86+
"audit-policy-file": []interface{}{
87+
path("AllRequestBodies"),
88+
},
89+
},
90+
},
91+
desiredProfile: stringPointer("NotExist"),
92+
expectedPath: path("AllRequestBodies"),
93+
errExpected: true,
94+
},
95+
{
96+
name: "WithCurrentEmptyAndDesiredInvalid",
97+
existingConfig: map[string]interface{}{},
98+
desiredProfile: stringPointer("NotExist"),
99+
expectedPath: "",
100+
errExpected: true,
101+
},
102+
{
103+
name: "WithCurrentAndDesiredBothSame",
104+
existingConfig: map[string]interface{}{
105+
"apiServerArguments": map[string]interface{}{
106+
"audit-policy-file": []interface{}{
107+
path("AllRequestBodies"),
108+
},
109+
},
110+
},
111+
desiredProfile: stringPointer("AllRequestBodies"),
112+
expectedPath: path("AllRequestBodies"),
113+
},
114+
{
115+
name: "WithCurrentSetAndAPIServerResourceMissing",
116+
existingConfig: map[string]interface{}{
117+
"apiServerArguments": map[string]interface{}{
118+
"audit-policy-file": []interface{}{
119+
path("AllRequestBodies"),
120+
},
121+
},
122+
},
123+
expectedPath: path("AllRequestBodies"),
124+
},
125+
{
126+
name: "WithCurrentNotSetAndAPIServerResourceMissing",
127+
existingConfig: map[string]interface{}{},
128+
expectedPath: "",
129+
},
130+
}
131+
132+
for _, test := range tests {
133+
t.Run(test.name, func(t *testing.T) {
134+
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
135+
if test.desiredProfile != nil {
136+
if err := indexer.Add(&configv1.APIServer{
137+
ObjectMeta: metav1.ObjectMeta{
138+
Name: "cluster",
139+
},
140+
Spec: configv1.APIServerSpec{
141+
Audit: configv1.Audit{
142+
Profile: configv1.AuditProfileType(*test.desiredProfile),
143+
},
144+
},
145+
}); err != nil {
146+
t.Fatal(err)
147+
}
148+
}
149+
listers := testLister{
150+
apiLister: configlistersv1.NewAPIServerLister(indexer),
151+
}
152+
153+
observer := NewAuditObserver(getter)
154+
recorder := events.NewInMemoryRecorder(t.Name())
155+
for i := 1; i <= 2; i++ {
156+
gotConfig, errs := observer(listers, recorder, test.existingConfig)
157+
158+
if test.errExpected && len(errs) == 0 {
159+
t.Errorf("expected errors, got %v", errs)
160+
}
161+
if !test.errExpected && len(errs) > 0 {
162+
t.Errorf("expected no errors, got %v", errs)
163+
}
164+
165+
gotPath := read(t, gotConfig)
166+
if test.expectedPath != gotPath {
167+
t.Errorf("audit path expected=%s got=%s", test.expectedPath, gotPath)
168+
}
169+
170+
// put the observed config back into existingConfig.
171+
if err := unstructured.SetNestedStringSlice(test.existingConfig, []string{gotPath}, auditPolicyFilePath...); err != nil {
172+
t.Errorf("failed to put the observed config into the current conig -%s", err)
173+
}
174+
}
175+
})
176+
}
177+
}
178+
179+
func path(profile string) string {
180+
return fmt.Sprintf("%s/%s", "/etc/kubernetes/static-pod-resources/configmaps/kube-apiserver-audit-policies",
181+
strings.ToLower(profile))
182+
}
183+
184+
func getter(profile string) (string, error) {
185+
if profile == "NotExist" {
186+
return "", fmt.Errorf("invalid profile - name=%s", profile)
187+
}
188+
189+
path := path(profile)
190+
return path, nil
191+
}
192+
193+
func stringPointer(s string) *string {
194+
p := &s
195+
return p
196+
}
197+
198+
func read(t *testing.T, config map[string]interface{}) string {
199+
// we expect only one key returned in the observed config.
200+
if len(config) > 1 {
201+
t.Fatal("expected observed config to have a single key 'apiServerArguments'")
202+
}
203+
204+
current, found, err := unstructured.NestedStringSlice(config, auditPolicyFilePath...)
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
209+
if !found {
210+
return ""
211+
}
212+
213+
if len(current) != 1 {
214+
t.Fatal("expected config to have only audit policy path defined")
215+
}
216+
217+
return current[0]
218+
}

vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_apiserver.crd.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_infrastructure.crd.yaml

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)