Skip to content

Commit

Permalink
apiserver/audit: split policy config map manifest into policy rule sl…
Browse files Browse the repository at this point in the history
…ices
  • Loading branch information
sttts committed Jun 22, 2021
1 parent 4a36118 commit 214ee5f
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 332 deletions.
203 changes: 118 additions & 85 deletions pkg/operator/apiserver/audit/audit_policies.go
@@ -1,134 +1,167 @@
package audit

import (
"encoding/json"
"errors"
"bytes"
"fmt"
"path"
"path/filepath"
"strings"

corev1 "k8s.io/api/core/v1"
kyaml "k8s.io/apimachinery/pkg/util/yaml"

configv1 "github.com/openshift/api/config/v1"
assets "github.com/openshift/library-go/pkg/operator/apiserver/audit/bindata"
libgoapiserver "github.com/openshift/library-go/pkg/operator/configobserver/apiserver"
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
"github.com/openshift/library-go/pkg/operator/resource/resourceread"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
"sigs.k8s.io/yaml"
)

const (
// AuditPoliciesConfigMapFileName hold the name of the file that you need to pass to WithAuditPolicies to get the audit policy config map
AuditPoliciesConfigMapFileName = "audit-policies-cm.yaml"
var (
basePolicy auditv1.Policy
profileRules = map[configv1.AuditProfileType][]auditv1.PolicyRule{}

auditScheme = runtime.NewScheme()
auditCodecs = serializer.NewCodecFactory(auditScheme)
auditYamlSerializer = json.NewYAMLSerializer(json.DefaultMetaFactory, auditScheme, auditScheme)

auditPolicyAsset = "pkg/operator/apiserver/audit/manifests/audit-policies-cm.yaml"
coreScheme = runtime.NewScheme()
coreCodecs = serializer.NewCodecFactory(coreScheme)
coreYamlSerializer = json.NewYAMLSerializer(json.DefaultMetaFactory, coreScheme, coreScheme)
)

func init() {
if err := auditv1.AddToScheme(auditScheme); err != nil {
panic(err)
}
if err := corev1.AddToScheme(coreScheme); err != nil {
panic(err)
}

bs, err := assets.Asset("pkg/operator/apiserver/audit/manifests/base-policy.yaml")
if err != nil {
panic(err)
}
if err := runtime.DecodeInto(coreCodecs.UniversalDecoder(auditv1.SchemeGroupVersion), bs, &basePolicy); err != nil {
panic(err)
}

for _, profile := range []configv1.AuditProfileType{configv1.AuditProfileDefaultType, configv1.WriteRequestBodiesAuditProfileType, configv1.AllRequestBodiesAuditProfileType} {
manifestName := fmt.Sprintf("%s-rules.yaml", strings.ToLower(string(profile)))
bs, err := assets.Asset(path.Join("pkg/operator/apiserver/audit/manifests", manifestName))
if err != nil {
panic(err)
}
var rules []auditv1.PolicyRule
if err := yaml.Unmarshal(bs, &rules); err != nil {
panic(err)
}
profileRules[profile] = rules
}
}

// DefaultPolicy brings back the default.yaml audit policy to init the api
func DefaultPolicy() ([]byte, error) {
// none is used on target name and namespace as it's not a key in the default.yaml
cm, err := GetAuditPolicies("none", "none")
policy, err := GetAuditPolicy(configv1.Audit{Profile: configv1.AuditProfileDefaultType})
if err != nil {
return nil, fmt.Errorf("failed to retreive audit policies - %w", err)
return nil, fmt.Errorf("failed to retreive default audit policy: %v", err)
}

cmData := []byte(cm.Data["default.yaml"])
if len(cmData) == 0 {
return nil, errors.New("failed to locate the default policy from configmap")
policy.Kind = "Policy"
policy.APIVersion = auditv1.SchemeGroupVersion.String()

var buf bytes.Buffer
if err := auditYamlSerializer.Encode(policy, &buf); err != nil {
return nil, err
}
return cmData, nil
return buf.Bytes(), nil
}

// WithAuditPolicies is meant to wrap a standard Asset function usually provided by an operator.
// It delegates to GetAuditPolicies when the filename matches the predicate for retrieving a audit policy config map for target namespace and name.
func WithAuditPolicies(targetName string, targetNamespace string, assetDelegateFunc resourceapply.AssetFunc) resourceapply.AssetFunc {
return func(file string) ([]byte, error) {
if file == AuditPoliciesConfigMapFileName {
return getRawAuditPolicies(targetName, targetNamespace)
if file != "audit-policies-cm.yaml" {
return assetDelegateFunc(file)
}

cm, err := GetAuditPolicies(targetName, targetNamespace)
if err != nil {
return nil, err
}
cm.Kind = "ConfigMap"
cm.APIVersion = "v1"

var buf bytes.Buffer
if err := coreYamlSerializer.Encode(cm, &buf); err != nil {
return nil, err
}
return assetDelegateFunc(file)
return buf.Bytes(), nil
}
}

// GetAuditPolicies returns a config map that holds the audit policies for the target namespaces and name
func GetAuditPolicies(targetName, targetNamespace string) (*corev1.ConfigMap, error) {
rawAuditPolicies, err := getRawAuditPolicies(targetName, targetNamespace)
if err != nil {
return nil, err
// GetAuditPolicy computes the audit policy for the given audit config.
// Note: the returned Policy has Kind and APIVersion not set. This is responsibility of the caller
// when serializing it.
func GetAuditPolicy(audit configv1.Audit) (*auditv1.Policy, error) {
p := basePolicy.DeepCopy()
p.Name = string(audit.Profile)

extraRules, ok := profileRules[audit.Profile]
if !ok {
return nil, fmt.Errorf("unknown audit profile %q", audit.Profile)
}
p.Rules = append(p.Rules, extraRules...)

return resourceread.ReadConfigMapV1OrDie(rawAuditPolicies), nil
return p, nil
}

// getRawAuditPolicies returns a raw config map that holds the audit policies for the target namespaces and name
func getRawAuditPolicies(targetName, targetNamespace string) ([]byte, error) {
if len(targetNamespace) == 0 {
return nil, errors.New("please specify the target namespace")
}
if len(targetName) == 0 {
return nil, errors.New("please specify the target name")
}
auditPoliciesTemplate, err := assets.Asset(auditPolicyAsset)
if err != nil {
return nil, err
// GetAuditPolicies returns a config map that holds the audit policies for the target namespaces and name.
// Note: the returned ConfigMap has Kind and APIVersion not set. This is responsibility of the caller
// when serializing it.
func GetAuditPolicies(targetName, targetNamespace string) (*corev1.ConfigMap, error) {
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: targetNamespace,
Name: targetName,
},
Data: map[string]string{},
}

r := strings.NewReplacer(
"${TARGET_NAME}", targetName,
"${TARGET_NAMESPACE}", targetNamespace,
)
auditPoliciesForTargetNs := []byte(r.Replace(string(auditPoliciesTemplate)))
for _, profile := range []configv1.AuditProfileType{configv1.AuditProfileDefaultType, configv1.WriteRequestBodiesAuditProfileType, configv1.AllRequestBodiesAuditProfileType} {
policy, err := GetAuditPolicy(configv1.Audit{Profile: profile})
if err != nil {
return nil, err
}

policy.Kind = "Policy"
policy.APIVersion = auditv1.SchemeGroupVersion.String()

var buf bytes.Buffer
if err := auditYamlSerializer.Encode(policy, &buf); err != nil {
return nil, err
}

cm.Data[fmt.Sprintf("%s.yaml", strings.ToLower(string(profile)))] = buf.String()
}

// we don't care about the output, just make sure that after replacing the ns it will serialize
resourceread.ReadConfigMapV1OrDie(auditPoliciesForTargetNs)
return auditPoliciesForTargetNs, nil
return &cm, nil
}

// NewAuditPolicyPathGetter returns a path getter for audit policy file mounted into the given path of a Pod as a directory.
//
// openshift-apiserver and oauth-apiserver mounts the audit policy ConfigMap into
// the above path inside the Pod.
func NewAuditPolicyPathGetter(path string) (libgoapiserver.AuditPolicyPathGetterFunc, error) {
return newAuditPolicyPathGetter(path)
}

func newAuditPolicyPathGetter(path string) (libgoapiserver.AuditPolicyPathGetterFunc, error) {
policies, err := readPolicyNamesFromAsset()
if err != nil {
return nil, err
}

return func(profile string) (string, error) {
// we expect the keys for audit profile in bindata to be in lower case and
// have a '.yaml' suffix.
key := fmt.Sprintf("%s.yaml", strings.ToLower(profile))
_, exists := policies[key]
if !exists {
return "", fmt.Errorf("invalid audit profile - key=%s", key)
manifestName := fmt.Sprintf("pkg/operator/apiserver/audit/manifests/%s-rules.yaml", strings.ToLower(profile))
if _, err := assets.Asset(manifestName); err != nil {
return "", fmt.Errorf("invalid audit profile %q", profile)
}

return fmt.Sprintf("%s/%s", path, key), nil
return filepath.Join(path, fmt.Sprintf("%s.yaml", strings.ToLower(profile))), nil
}, nil
}

func readPolicyNamesFromAsset() (map[string]struct{}, error) {
bytes, err := assets.Asset(auditPolicyAsset)
if err != nil {
return nil, fmt.Errorf("failed to load asset asset=%s - %s", auditPolicyAsset, err)
}

rawJSON, err := kyaml.ToJSON(bytes)
if err != nil {
return nil, fmt.Errorf("failed to convert asset yaml to JSON asset=%s - %s", auditPolicyAsset, err)
}

cm := corev1.ConfigMap{}
if err := json.Unmarshal(rawJSON, &cm); err != nil {
return nil, fmt.Errorf("failed to unmarshal audit policy asset=%s - %s", auditPolicyAsset, err)
}

policies := map[string]struct{}{}
for key := range cm.Data {
policies[key] = struct{}{}
}

return policies, nil
}
64 changes: 56 additions & 8 deletions pkg/operator/apiserver/audit/audit_policies_test.go
Expand Up @@ -6,10 +6,11 @@ import (
"os"
"testing"

"github.com/openshift/library-go/pkg/operator/resource/resourceread"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/util/diff"

"github.com/openshift/library-go/pkg/operator/resource/resourceread"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
"sigs.k8s.io/yaml"
)

func TestWithAuditPolicies(t *testing.T) {
Expand Down Expand Up @@ -53,8 +54,32 @@ func TestWithAuditPolicies(t *testing.T) {
actualAuditPolicies := resourceread.ReadConfigMapV1OrDie(actualAuditPoliciesData)
goldenAuditPoliciesData := readBytesFromFile(t, scenario.goldenFile)
goldenAuditPolicies := resourceread.ReadConfigMapV1OrDie(goldenAuditPoliciesData)
if !equality.Semantic.DeepEqual(actualAuditPolicies, goldenAuditPolicies) {
t.Errorf("created config map is different from the expected one (file) : %s", diff.ObjectDiff(actualAuditPolicies, goldenAuditPolicies))

if got, expected := len(actualAuditPolicies.Data), len(goldenAuditPolicies.Data); got != expected {
t.Errorf("unexpected number of policies %d, expected %d", got, expected)
}

for name, bs := range actualAuditPolicies.Data {
var got auditv1.Policy
if err := yaml.Unmarshal([]byte(bs), &got); err != nil {
t.Errorf("failed to unmarshal policy %q: %v", name, err)
continue
}

bs, ok := goldenAuditPolicies.Data[name]
if !ok {
t.Errorf("unexpected policy %q", name)
continue
}
var expected auditv1.Policy
if err := yaml.Unmarshal([]byte(bs), &expected); err != nil {
t.Errorf("failed to unmarshal golden policy %q: %v", name, err)
continue
}

if !equality.Semantic.DeepEqual(got, expected) {
t.Errorf("policy %q differs: %s", name, diff.ObjectDiff(expected, got))
}
}
}
if err := scenario.delegate.Validate(); err != nil {
Expand All @@ -81,18 +106,41 @@ func TestGetAuditPolicies(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
// act
actualAuditPoliciesData, err := getRawAuditPolicies(scenario.targetName, scenario.targetNamespace)
actualAuditPolicies, err := GetAuditPolicies(scenario.targetName, scenario.targetNamespace)
if err != nil {
t.Fatal(err)
}

// validate
if len(scenario.goldenFile) > 0 {
actualAuditPolicies := resourceread.ReadConfigMapV1OrDie(actualAuditPoliciesData)
goldenAuditPoliciesData := readBytesFromFile(t, scenario.goldenFile)
goldenAuditPolicies := resourceread.ReadConfigMapV1OrDie(goldenAuditPoliciesData)
if !equality.Semantic.DeepEqual(actualAuditPolicies, goldenAuditPolicies) {
t.Errorf("created config map is different from the expected one (file) : %s", diff.ObjectDiff(actualAuditPolicies, goldenAuditPolicies))

if got, expected := len(actualAuditPolicies.Data), len(goldenAuditPolicies.Data); got != expected {
t.Errorf("unexpected number of policies %d, expected %d", got, expected)
}

for name, bs := range actualAuditPolicies.Data {
var got auditv1.Policy
if err := yaml.Unmarshal([]byte(bs), &got); err != nil {
t.Errorf("failed to unmarshal policy %q: %v", name, err)
continue
}

bs, ok := goldenAuditPolicies.Data[name]
if !ok {
t.Errorf("unexpected policy %q", name)
continue
}
var expected auditv1.Policy
if err := yaml.Unmarshal([]byte(bs), &expected); err != nil {
t.Errorf("failed to unmarshal golden policy %q: %v", name, err)
continue
}

if !equality.Semantic.DeepEqual(got, expected) {
t.Errorf("policy %q differs: %s", name, diff.ObjectDiff(expected, got))
}
}
}
})
Expand Down

0 comments on commit 214ee5f

Please sign in to comment.