mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-04 23:17:50 +00:00
adds dynamic audit api
This commit is contained in:
200
pkg/apis/auditregistration/validation/validation.go
Normal file
200
pkg/apis/auditregistration/validation/validation.go
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/auditregistration"
|
||||
)
|
||||
|
||||
// ValidateAuditSink validates the AuditSinks
|
||||
func ValidateAuditSink(as *auditregistration.AuditSink) field.ErrorList {
|
||||
allErrs := genericvalidation.ValidateObjectMeta(&as.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
allErrs = append(allErrs, ValidateAuditSinkSpec(as.Spec, field.NewPath("spec"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateAuditSinkSpec validates the sink spec for audit
|
||||
func ValidateAuditSinkSpec(s auditregistration.AuditSinkSpec, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
allErrs = append(allErrs, ValidatePolicy(s.Policy, field.NewPath("policy"))...)
|
||||
allErrs = append(allErrs, ValidateWebhook(s.Webhook, field.NewPath("webhook"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateWebhook validates the webhook
|
||||
func ValidateWebhook(w auditregistration.Webhook, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if w.Throttle != nil {
|
||||
allErrs = append(allErrs, ValidateWebhookThrottleConfig(w.Throttle, fldPath.Child("throttle"))...)
|
||||
}
|
||||
allErrs = append(allErrs, ValidateWebhookClientConfig(&w.ClientConfig, fldPath.Child("clientConfig"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateWebhookThrottleConfig validates the throttle config
|
||||
func ValidateWebhookThrottleConfig(c *auditregistration.WebhookThrottleConfig, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if c.QPS != nil && *c.QPS <= 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("qps"), c.QPS, "qps must be a positive number"))
|
||||
}
|
||||
if c.Burst != nil && *c.Burst <= 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("burst"), c.Burst, "burst must be a positive number"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateWebhookClientConfig validates the WebhookClientConfig
|
||||
// note: this is largely copy/paste inheritance from admissionregistration with subtle changes
|
||||
func ValidateWebhookClientConfig(cc *auditregistration.WebhookClientConfig, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if (cc.URL == nil) == (cc.Service == nil) {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "exactly one of url or service is required"))
|
||||
}
|
||||
|
||||
if cc.URL != nil {
|
||||
const form = "; desired format: https://host[/path]"
|
||||
if u, err := url.Parse(*cc.URL); err != nil {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "url must be a valid URL: "+err.Error()+form))
|
||||
} else {
|
||||
if len(u.Host) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.Host, "host must be provided"+form))
|
||||
}
|
||||
if u.User != nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.User.String(), "user information is not permitted in the URL"))
|
||||
}
|
||||
if len(u.Fragment) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.Fragment, "fragments are not permitted in the URL"))
|
||||
}
|
||||
if len(u.RawQuery) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.RawQuery, "query parameters are not permitted in the URL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cc.Service != nil {
|
||||
allErrors = append(allErrors, validateWebhookService(cc.Service, fldPath.Child("service"))...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// note: this is copy/paste inheritance from admissionregistration
|
||||
func validateWebhookService(svc *auditregistration.ServiceReference, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
|
||||
if len(svc.Name) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
|
||||
}
|
||||
|
||||
if len(svc.Namespace) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
|
||||
}
|
||||
|
||||
if svc.Path == nil {
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// TODO: replace below with url.Parse + verifying that host is empty?
|
||||
|
||||
urlPath := *svc.Path
|
||||
if urlPath == "/" || len(urlPath) == 0 {
|
||||
return allErrors
|
||||
}
|
||||
if urlPath == "//" {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
|
||||
return allErrors
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(urlPath, "/") {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
|
||||
}
|
||||
|
||||
urlPathToCheck := urlPath[1:]
|
||||
if strings.HasSuffix(urlPathToCheck, "/") {
|
||||
urlPathToCheck = urlPathToCheck[:len(urlPathToCheck)-1]
|
||||
}
|
||||
steps := strings.Split(urlPathToCheck, "/")
|
||||
for i, step := range steps {
|
||||
if len(step) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
|
||||
continue
|
||||
}
|
||||
failures := validation.IsDNS1123Subdomain(step)
|
||||
for _, failure := range failures {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// ValidatePolicy validates the audit policy
|
||||
func ValidatePolicy(policy auditregistration.Policy, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
allErrs = append(allErrs, validateStages(policy.Stages, fldPath.Child("stages"))...)
|
||||
allErrs = append(allErrs, validateLevel(policy.Level, fldPath.Child("level"))...)
|
||||
if policy.Level != auditregistration.LevelNone && len(policy.Stages) == 0 {
|
||||
return field.ErrorList{field.Required(fldPath.Child("stages"), "")}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
var validLevels = sets.NewString(
|
||||
string(auditregistration.LevelNone),
|
||||
string(auditregistration.LevelMetadata),
|
||||
string(auditregistration.LevelRequest),
|
||||
string(auditregistration.LevelRequestResponse),
|
||||
)
|
||||
|
||||
var validStages = sets.NewString(
|
||||
string(auditregistration.StageRequestReceived),
|
||||
string(auditregistration.StageResponseStarted),
|
||||
string(auditregistration.StageResponseComplete),
|
||||
string(auditregistration.StagePanic),
|
||||
)
|
||||
|
||||
func validateLevel(level auditregistration.Level, fldPath *field.Path) field.ErrorList {
|
||||
if string(level) == "" {
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
if !validLevels.Has(string(level)) {
|
||||
return field.ErrorList{field.NotSupported(fldPath, level, validLevels.List())}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateStages(stages []auditregistration.Stage, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
for i, stage := range stages {
|
||||
if !validStages.Has(string(stage)) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), string(stage), "allowed stages are "+strings.Join(validStages.List(), ",")))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateAuditSinkUpdate validates an update to the object
|
||||
func ValidateAuditSinkUpdate(newC, oldC *auditregistration.AuditSink) field.ErrorList {
|
||||
return ValidateAuditSink(newC)
|
||||
}
|
||||
324
pkg/apis/auditregistration/validation/validation_test.go
Normal file
324
pkg/apis/auditregistration/validation/validation_test.go
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/auditregistration"
|
||||
)
|
||||
|
||||
func TestValidateAuditSink(t *testing.T) {
|
||||
testQPS := int64(10)
|
||||
testURL := "http://localhost"
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf auditregistration.AuditSink
|
||||
numErr int
|
||||
}{
|
||||
{
|
||||
name: "should pass full config",
|
||||
conf: auditregistration.AuditSink{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myconf",
|
||||
},
|
||||
Spec: auditregistration.AuditSinkSpec{
|
||||
Policy: auditregistration.Policy{
|
||||
Level: auditregistration.LevelRequest,
|
||||
Stages: []auditregistration.Stage{
|
||||
auditregistration.StageRequestReceived,
|
||||
},
|
||||
},
|
||||
Webhook: auditregistration.Webhook{
|
||||
Throttle: &auditregistration.WebhookThrottleConfig{
|
||||
QPS: &testQPS,
|
||||
},
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: &testURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
numErr: 0,
|
||||
},
|
||||
{
|
||||
name: "should fail no policy",
|
||||
conf: auditregistration.AuditSink{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myconf",
|
||||
},
|
||||
Spec: auditregistration.AuditSinkSpec{
|
||||
Webhook: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: &testURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
numErr: 1,
|
||||
},
|
||||
{
|
||||
name: "should fail no webhook",
|
||||
conf: auditregistration.AuditSink{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myconf",
|
||||
},
|
||||
Spec: auditregistration.AuditSinkSpec{
|
||||
Policy: auditregistration.Policy{
|
||||
Level: auditregistration.LevelMetadata,
|
||||
Stages: []auditregistration.Stage{
|
||||
auditregistration.StageRequestReceived,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
numErr: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
errs := ValidateAuditSink(&test.conf)
|
||||
require.Len(t, errs, test.numErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePolicy(t *testing.T) {
|
||||
successCases := []auditregistration.Policy{}
|
||||
successCases = append(successCases, auditregistration.Policy{ // Policy with omitStages and level
|
||||
Level: auditregistration.LevelRequest,
|
||||
Stages: []auditregistration.Stage{
|
||||
auditregistration.Stage("RequestReceived"),
|
||||
auditregistration.Stage("ResponseStarted"),
|
||||
},
|
||||
})
|
||||
successCases = append(successCases, auditregistration.Policy{Level: auditregistration.LevelNone}) // Policy with none level only
|
||||
|
||||
for i, policy := range successCases {
|
||||
if errs := ValidatePolicy(policy, field.NewPath("policy")); len(errs) != 0 {
|
||||
t.Errorf("[%d] Expected policy %#v to be valid: %v", i, policy, errs)
|
||||
}
|
||||
}
|
||||
|
||||
errorCases := []auditregistration.Policy{}
|
||||
errorCases = append(errorCases, auditregistration.Policy{}) // Empty policy // Policy with missing level
|
||||
errorCases = append(errorCases, auditregistration.Policy{Stages: []auditregistration.Stage{ // Policy with invalid stages
|
||||
auditregistration.Stage("Bad")}})
|
||||
errorCases = append(errorCases, auditregistration.Policy{Level: auditregistration.Level("invalid")}) // Policy with bad level
|
||||
errorCases = append(errorCases, auditregistration.Policy{Level: auditregistration.LevelMetadata}) // Policy without stages
|
||||
|
||||
for i, policy := range errorCases {
|
||||
if errs := ValidatePolicy(policy, field.NewPath("policy")); len(errs) == 0 {
|
||||
t.Errorf("[%d] Expected policy %#v to be invalid!", i, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
func TestValidateWebhookConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config auditregistration.Webhook
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "both service and URL missing",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{},
|
||||
},
|
||||
expectedError: `exactly one of`,
|
||||
},
|
||||
{
|
||||
name: "both service and URL provided",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
},
|
||||
URL: strPtr("example.com/k8s/webhook"),
|
||||
},
|
||||
},
|
||||
expectedError: `webhook.clientConfig.url: Required value: exactly one of url or service is required`,
|
||||
},
|
||||
{
|
||||
name: "blank URL",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr(""),
|
||||
},
|
||||
},
|
||||
expectedError: `webhook.clientConfig.url: Invalid value: "": host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "missing host",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr("https:///fancy/webhook"),
|
||||
},
|
||||
},
|
||||
expectedError: `host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "fragment",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com/#bookmark"),
|
||||
},
|
||||
},
|
||||
expectedError: `"bookmark": fragments are not permitted`,
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com?arg=value"),
|
||||
},
|
||||
},
|
||||
expectedError: `"arg=value": query parameters are not permitted`,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://harry.potter@example.com/"),
|
||||
},
|
||||
},
|
||||
expectedError: `"harry.potter": user information is not permitted`,
|
||||
},
|
||||
{
|
||||
name: "just totally wrong",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"),
|
||||
},
|
||||
},
|
||||
expectedError: `host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "path must start with slash",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("foo/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`,
|
||||
},
|
||||
{
|
||||
name: "path accepts slash",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: ``,
|
||||
},
|
||||
{
|
||||
name: "path accepts no trailing slash",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: ``,
|
||||
},
|
||||
{
|
||||
name: "path fails //",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("//"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`,
|
||||
},
|
||||
{
|
||||
name: "path no empty step",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo//bar/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`,
|
||||
}, {
|
||||
name: "path no empty step 2",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo/bar//"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`,
|
||||
},
|
||||
{
|
||||
name: "path no non-subdomain",
|
||||
config: auditregistration.Webhook{
|
||||
ClientConfig: auditregistration.WebhookClientConfig{
|
||||
Service: &auditregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/apis/foo.bar/v1alpha1/--bad"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a DNS-1123 subdomain`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
errs := ValidateWebhook(test.config, field.NewPath("webhook"))
|
||||
err := errs.ToAggregate()
|
||||
if err != nil {
|
||||
if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" {
|
||||
t.Errorf("expected to contain \nerr: %s \ngot: %s", e, a)
|
||||
}
|
||||
} else {
|
||||
if test.expectedError != "" {
|
||||
t.Errorf("unexpected no error, expected to contain %s", test.expectedError)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user