diff --git a/docs.md b/docs.md index bfbbf578f..22a1c4215 100644 --- a/docs.md +++ b/docs.md @@ -1,3 +1,27 @@ +# auditlog.cattle.io/v1 + +## AuditPolicy + +### Validation Checks + +#### Invalid Fields - Create + +Users cannot create an `AuditPolicy` which violates the following constraints: + +- `.Spec.Filters[].Action` must be one of `allow` or `deny` +- `.Spec.Filters[].RequestURI` must be valid regex +- `.Spec.AdditionalRedactions[].Headers[]` must be valid regez +- `.Spec.AdditionalRedactions[].Paths[]` must be valid jsonpath + +#### Invalid Fields - Update + +Users cannot update an `AuditPolicy` which violates the following constraints: + +- `.Spec.Filters[].Action` must be one of `allow` or `deny` +- `.Spec.Filters[].RequestURI` must be valid regex +- `.Spec.AdditionalRedactions[].Headers[]` must be valid regez +- `.Spec.AdditionalRedactions[].Paths[]` must be valid jsonpath + # catalog.cattle.io/v1 ## ClusterRepo diff --git a/go.mod b/go.mod index f3024c75e..aebbcefe6 100644 --- a/go.mod +++ b/go.mod @@ -43,8 +43,9 @@ require ( github.com/go-ldap/ldap/v3 v3.4.10 github.com/gorilla/mux v1.8.1 github.com/rancher/dynamiclistener v0.7.0 + github.com/rancher/jsonpath v0.0.0-20250620213443-ad24535cf0c1 github.com/rancher/lasso v0.2.3-rc3 - github.com/rancher/rancher/pkg/apis v0.0.0-20250624062103-8bf56b046af7 + github.com/rancher/rancher/pkg/apis v0.0.0-20250628053911-e6fa86c1c800 github.com/rancher/rke v1.8.0-rc.4 github.com/rancher/wrangler/v3 v3.2.2-rc.3 github.com/robfig/cron v1.2.0 @@ -59,7 +60,7 @@ require ( k8s.io/client-go v12.0.0+incompatible k8s.io/kubernetes v1.33.1 k8s.io/pod-security-admission v0.32.1 - k8s.io/utils v0.0.0-20241210054802-24370beab758 + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/yaml v1.4.0 ) @@ -98,7 +99,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/rancher/aks-operator v1.12.0-rc.1 // indirect github.com/rancher/eks-operator v1.12.0-rc.1 // indirect - github.com/rancher/fleet/pkg/apis v0.12.0 // indirect + github.com/rancher/fleet/pkg/apis v0.13.0-alpha.5 // indirect github.com/rancher/gke-operator v1.12.0-rc.1 // indirect github.com/rancher/norman v0.6.1 // indirect github.com/spf13/pflag v1.0.6 // indirect diff --git a/go.sum b/go.sum index e3652650c..e82e28ffd 100644 --- a/go.sum +++ b/go.sum @@ -126,16 +126,18 @@ github.com/rancher/dynamiclistener v0.7.0 h1:+jyfZ4lVamc1UbKWo8V8dhSPtCgRZYaY8nm github.com/rancher/dynamiclistener v0.7.0/go.mod h1:Q2YA42xp7Xc69JiSlJ8GpvLvze261T0iQ/TP4RdMCYk= github.com/rancher/eks-operator v1.12.0-rc.1 h1:xQ9rMqz+T3UOrGsUnjr3j49SRfEM1FJJ+DDCDb7h5II= github.com/rancher/eks-operator v1.12.0-rc.1/go.mod h1:w7ZzRD/Hhxqltlz7XWAgvviYWhmXpwwB7mfTPOZu99E= -github.com/rancher/fleet/pkg/apis v0.12.0 h1:ZzDB63LFLrtwu0wOMDSrmtboixEnwPnNS4sY0KfCr9M= -github.com/rancher/fleet/pkg/apis v0.12.0/go.mod h1:aPeCke/1zE63hh4rojpm12hc0tZfwuEn0WXrhM4/PXM= +github.com/rancher/fleet/pkg/apis v0.13.0-alpha.5 h1:mSMSgEOaf3vPEFJ0/F/IC1XX+Dda56PebVuafZcR+Yo= +github.com/rancher/fleet/pkg/apis v0.13.0-alpha.5/go.mod h1:Qvt5nNrVgwboBZJy3dMNcVC9fncUFKaVPpbj+LdeYEs= github.com/rancher/gke-operator v1.12.0-rc.1 h1:fMk5S4cvRfZvreaSz/NM9iWewWI8QIOoYI4L0ZHakyo= github.com/rancher/gke-operator v1.12.0-rc.1/go.mod h1:O1/6RFU05XW6vOEJ0a7xvboBSy5X6EE8DZqzikMK9tg= +github.com/rancher/jsonpath v0.0.0-20250620213443-ad24535cf0c1 h1:vRtxuvIF0UarXIAwGYNwSFoRYJadVnOrD8kx2qrZyN8= +github.com/rancher/jsonpath v0.0.0-20250620213443-ad24535cf0c1/go.mod h1:xavYr3cNyyAeA72nMVB60+q/EJ8Anu+loQWFJyXOeP8= github.com/rancher/lasso v0.2.3-rc3 h1:kkYnARdFeY6A9E2XnjfQbG8CssHQwobPMIFqPRGpVxc= github.com/rancher/lasso v0.2.3-rc3/go.mod h1:G+KeeOaKRjp+qGp0bV6VbLhYrq1vHbJPbDh40ejg5yE= github.com/rancher/norman v0.6.1 h1:JVIxkWZcbxtrcSK4WIwsmyB07TzTvoqqf/R0fZB7ylk= github.com/rancher/norman v0.6.1/go.mod h1:jRcnEruyY6olqtImy+q+zw0/iXX7YqLF4/K4kh5hR40= -github.com/rancher/rancher/pkg/apis v0.0.0-20250624062103-8bf56b046af7 h1:h2PdLfqEiJJyYv6Fy823cfX5rJdt2lfa4i9RVYrbKcE= -github.com/rancher/rancher/pkg/apis v0.0.0-20250624062103-8bf56b046af7/go.mod h1:OCn4rNvTXZ3mki8aaZorlIz8UukEq5gRqphXxt9G3I8= +github.com/rancher/rancher/pkg/apis v0.0.0-20250628053911-e6fa86c1c800 h1:X6b36q0MT30IaO4TPEuNOq1AcqF/Vr1Qi+Es7MRA4/Q= +github.com/rancher/rancher/pkg/apis v0.0.0-20250628053911-e6fa86c1c800/go.mod h1:OCn4rNvTXZ3mki8aaZorlIz8UukEq5gRqphXxt9G3I8= github.com/rancher/rke v1.7.2 h1:+2fcl0gCjRHzf1ev9C9ptQ1pjYbDngC1Qv8V/0ki/dk= github.com/rancher/rke v1.7.2/go.mod h1:+x++Mvl0A3jIzNLiu8nkraqZXiHg6VPWv0Xl4iQCg+A= github.com/rancher/wrangler/v3 v3.2.2-rc.3 h1:ObcqAxQkQFP6r1YI3zJhi9v9PE+UUNNZpelz6NSpQnc= @@ -339,8 +341,8 @@ k8s.io/kubernetes v1.33.1 h1:86+VVY/f11taZdpEZrNciLw1MIQhu6BFXf/OMFn5EUg= k8s.io/kubernetes v1.33.1/go.mod h1:2nWuPk0seE4+6sd0x60wQ6rYEXcV7SoeMbU0YbFm/5k= k8s.io/pod-security-admission v0.33.1 h1:amePfcTDgLHB1wpZFIO7chW3Pc/ikeYbniuMTQEcaB4= k8s.io/pod-security-admission v0.33.1/go.mod h1:3gSyP5JPgte2EHjQheA81299vISL6D7DDvk2m9RQj6k= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/cluster-api v1.10.2 h1:xfvtNu4Fy/41grL0ryH5xSKQjpJEWdO8HiV2lPCCozQ= sigs.k8s.io/cluster-api v1.10.2/go.mod h1:/b9Un5Imprib6S7ZOcJitC2ep/5wN72b0pXpMQFfbTw= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index 2c112cbbc..679d054a3 100644 --- a/pkg/codegen/main.go +++ b/pkg/codegen/main.go @@ -9,6 +9,7 @@ import ( "strings" "text/template" + auditlogv1 "github.com/rancher/rancher/pkg/apis/auditlog.cattle.io/v1" catalogv1 "github.com/rancher/rancher/pkg/apis/catalog.cattle.io/v1" v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" v1 "github.com/rancher/rancher/pkg/apis/provisioning.cattle.io/v1" @@ -109,7 +110,13 @@ func main() { &rbacv1.ClusterRole{}, &rbacv1.ClusterRoleBinding{}, }, - }}); err != nil { + }, + "auditlog.cattle.io": { + Types: []interface{}{ + &auditlogv1.AuditPolicy{}, + }, + }, + }); err != nil { fmt.Printf("ERROR: %v\n", err) } } diff --git a/pkg/generated/objects/auditlog.cattle.io/v1/objects.go b/pkg/generated/objects/auditlog.cattle.io/v1/objects.go new file mode 100644 index 000000000..bb8431203 --- /dev/null +++ b/pkg/generated/objects/auditlog.cattle.io/v1/objects.go @@ -0,0 +1,62 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + "github.com/rancher/rancher/pkg/apis/auditlog.cattle.io/v1" + admissionv1 "k8s.io/api/admission/v1" +) + +// AuditPolicyOldAndNewFromRequest gets the old and new AuditPolicy objects, respectively, from the webhook request. +// If the request is a Delete operation, then the new object is the zero value for AuditPolicy. +// Similarly, if the request is a Create operation, then the old object is the zero value for AuditPolicy. +func AuditPolicyOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.AuditPolicy, *v1.AuditPolicy, error) { + if request == nil { + return nil, nil, fmt.Errorf("nil request") + } + + object := &v1.AuditPolicy{} + oldObject := &v1.AuditPolicy{} + + if request.Operation != admissionv1.Delete { + err := json.Unmarshal(request.Object.Raw, object) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err) + } + } + + if request.Operation == admissionv1.Create { + return oldObject, object, nil + } + + err := json.Unmarshal(request.OldObject.Raw, oldObject) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err) + } + + return oldObject, object, nil +} + +// AuditPolicyFromRequest returns a AuditPolicy object from the webhook request. +// If the operation is a Delete operation, then the old object is returned. +// Otherwise, the new object is returned. +func AuditPolicyFromRequest(request *admissionv1.AdmissionRequest) (*v1.AuditPolicy, error) { + if request == nil { + return nil, fmt.Errorf("nil request") + } + + object := &v1.AuditPolicy{} + raw := request.Object.Raw + + if request.Operation == admissionv1.Delete { + raw = request.OldObject.Raw + } + + err := json.Unmarshal(raw, object) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal request object: %w", err) + } + + return object, nil +} diff --git a/pkg/resources/auditlog.cattle.io/v1/auditpolicy/AuditPolicy.md b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/AuditPolicy.md new file mode 100644 index 000000000..391aec72f --- /dev/null +++ b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/AuditPolicy.md @@ -0,0 +1,19 @@ +## Validation Checks + +### Invalid Fields - Create + +Users cannot create an `AuditPolicy` which violates the following constraints: + +- `.Spec.Filters[].Action` must be one of `allow` or `deny` +- `.Spec.Filters[].RequestURI` must be valid regex +- `.Spec.AdditionalRedactions[].Headers[]` must be valid regez +- `.Spec.AdditionalRedactions[].Paths[]` must be valid jsonpath + +### Invalid Fields - Update + +Users cannot update an `AuditPolicy` which violates the following constraints: + +- `.Spec.Filters[].Action` must be one of `allow` or `deny` +- `.Spec.Filters[].RequestURI` must be valid regex +- `.Spec.AdditionalRedactions[].Headers[]` must be valid regez +- `.Spec.AdditionalRedactions[].Paths[]` must be valid jsonpath \ No newline at end of file diff --git a/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator.go b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator.go new file mode 100644 index 000000000..4ef701564 --- /dev/null +++ b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator.go @@ -0,0 +1,121 @@ +package v1 + +import ( + "errors" + "fmt" + "regexp" + + jsonpath "github.com/rancher/jsonpath/pkg" + auditlogv1 "github.com/rancher/rancher/pkg/apis/auditlog.cattle.io/v1" + "github.com/rancher/webhook/pkg/admission" + v1 "github.com/rancher/webhook/pkg/generated/objects/auditlog.cattle.io/v1" + admissionv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +var gvr = schema.GroupVersionResource{ + Group: auditlogv1.SchemeGroupVersion.Group, + Version: auditlogv1.SchemeGroupVersion.Version, + Resource: "auditpolicies", +} + +var _ admission.ValidatingAdmissionHandler = &validator{} + +type validator struct { +} + +// Admitters implements admission.ValidatingAdmissionHandler. +func (v *validator) Admitters() []admission.Admitter { + return []admission.Admitter{ + &admitter{}, + } +} + +// GVR implements admission.ValidatingAdmissionHandler. +func (v *validator) GVR() schema.GroupVersionResource { + return gvr +} + +// Operations implements admission.ValidatingAdmissionHandler. +func (v *validator) Operations() []admissionregistrationv1.OperationType { + return []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + } +} + +// ValidatingWebhook implements admission.ValidatingAdmissionHandler. +func (v *validator) ValidatingWebhook(clientConfig admissionregistrationv1.WebhookClientConfig) []admissionregistrationv1.ValidatingWebhook { + return []admissionregistrationv1.ValidatingWebhook{ + *admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations()), + } +} + +type admitter struct { +} + +func (a *admitter) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) { + if req.Operation != admissionv1.Create && req.Operation != admissionv1.Update { + return admission.ResponseAllowed(), nil + } + + policy, err := v1.AuditPolicyFromRequest(&req.AdmissionRequest) + if err != nil { + return nil, fmt.Errorf("failed to get AuditPolicy from request: %w", err) + } + + path := field.NewPath("auditpolicy", "spec") + + var fieldErr *field.Error + if err := a.validateFields(policy, path); err != nil { + if errors.As(err, &fieldErr) { + return admission.ResponseBadRequest(fieldErr.Error()), nil + } + + return nil, fmt.Errorf("failed to validate fields on AuditPolicy") + } + + return nil, fmt.Errorf("nyi") +} + +func (a *admitter) validateFields(policy *auditlogv1.AuditPolicy, path *field.Path) error { + if policy.Spec.Verbosity.Level < 0 || policy.Spec.Verbosity.Level > 3 { + return field.Invalid(path.Child("verbosity", "level"), policy.Spec.Verbosity.Level, ".spec.verbosity.level must be >= 0 or <= 3") + } + + for i, filter := range policy.Spec.Filters { + path := path.Child("filters").Index(i) + + if filter.Action != auditlogv1.FilterActionAllow && filter.Action != auditlogv1.FilterActionDeny { + return field.NotSupported(path, filter.Action, []string{string(auditlogv1.FilterActionAllow), string(auditlogv1.FilterActionDeny)}) + } + + if _, err := regexp.Compile(filter.RequestURI); err != nil { + return field.Invalid(path, filter.RequestURI, err.Error()) + } + } + + for i, redaction := range policy.Spec.AdditionalRedactions { + path := path.Child("additionalRedactions").Index(i) + + for j, header := range redaction.Headers { + path := path.Child("headers").Index(j) + + if _, err := regexp.Compile(header); err != nil { + return field.Invalid(path, header, err.Error()) + } + } + + for j, jp := range redaction.Paths { + path := path.Child("paths").Index(j) + + if _, err := jsonpath.Parse(jp); err != nil { + return field.Invalid(path, jp, err.Error()) + } + } + } + + return nil +} diff --git a/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator_test.go b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator_test.go new file mode 100644 index 000000000..a74735b61 --- /dev/null +++ b/pkg/resources/auditlog.cattle.io/v1/auditpolicy/validator_test.go @@ -0,0 +1,214 @@ +package v1 + +import ( + "testing" + + auditlogv1 "github.com/rancher/rancher/pkg/apis/auditlog.cattle.io/v1" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestAdmitterValidateFields(t *testing.T) { + type testCase struct { + Name string + Policy *auditlogv1.AuditPolicy + Expected error + } + + cases := []testCase{ + { + Name: "filter action allow is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + { + Action: auditlogv1.FilterActionAllow, + }, + }, + }, + }, + }, + { + Name: "filter action deny is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + { + Action: auditlogv1.FilterActionDeny, + }, + }, + }, + }, + }, + { + Name: "invalid filter action is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + { + Action: "you shall not pass", + }, + }, + }, + }, + Expected: field.NotSupported(field.NewPath("auditpolicy", "spec", "filters").Index(0), "you shall not pass", []string{string(auditlogv1.FilterActionAllow), string(auditlogv1.FilterActionDeny)}), + }, + { + Name: "empty filter action is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + {}, + }, + }, + }, + Expected: field.NotSupported(field.NewPath("auditpolicy", "spec", "filters").Index(0), "", []string{string(auditlogv1.FilterActionAllow), string(auditlogv1.FilterActionDeny)}), + }, + { + Name: "valid filter request uri regex is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + { + Action: auditlogv1.FilterActionAllow, + RequestURI: "/some/endoint/.*", + }, + }, + }, + }, + }, + { + Name: "invalid filter request uri regex is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Filters: []auditlogv1.Filter{ + { + Action: auditlogv1.FilterActionAllow, + RequestURI: "*", + }, + }, + }, + }, + Expected: field.Invalid(field.NewPath("auditpolicy", "spec", "filters").Index(0), "*", "error parsing regexp: missing argument to repetition operator: `*`"), + }, + + { + Name: "valid header regex is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + AdditionalRedactions: []auditlogv1.Redaction{ + { + Headers: []string{ + ".*", + }, + }, + }, + }, + }, + }, + { + Name: "invalid header regex is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + AdditionalRedactions: []auditlogv1.Redaction{ + { + Headers: []string{ + "*", + }, + }, + }, + }, + }, + Expected: field.Invalid(field.NewPath("auditpolicy", "spec", "additionalRedactions").Index(0).Child("headers").Index(0), "*", "error parsing regexp: missing argument to repetition operator: `*`"), + }, + { + Name: "valid jsonpath is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + AdditionalRedactions: []auditlogv1.Redaction{ + { + Paths: []string{ + "$..*", + }, + }, + }, + }, + }, + }, + { + Name: "invalid jsonpath is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + AdditionalRedactions: []auditlogv1.Redaction{ + { + Paths: []string{ + "..*", + }, + }, + }, + }, + }, + Expected: field.Invalid(field.NewPath("auditpolicy", "spec", "additionalRedactions").Index(0).Child("paths").Index(0), "..*", "paths must begin with the root object identifier: '$'"), + }, + + { + Name: "verbosity level 0 is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Verbosity: auditlogv1.LogVerbosity{ + Level: 0, + }, + }, + }, + }, + { + Name: "verbosity level 3 is valid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Verbosity: auditlogv1.LogVerbosity{ + Level: 3, + }, + }, + }, + }, + { + Name: "verbosity level -1 is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Verbosity: auditlogv1.LogVerbosity{ + Level: -1, + }, + }, + }, + Expected: field.Invalid(field.NewPath("auditpolicy", "spec", "verbosity", "level"), -1, ".spec.verbosity.level must be >= 0 or <= 3"), + }, + { + Name: "verbosity level 4 is invalid", + Policy: &auditlogv1.AuditPolicy{ + Spec: auditlogv1.AuditPolicySpec{ + Verbosity: auditlogv1.LogVerbosity{ + Level: 4, + }, + }, + }, + Expected: field.Invalid(field.NewPath("auditpolicy", "spec", "verbosity", "level"), 4, ".spec.verbosity.level must be >= 0 or <= 3"), + }, + } + + a := admitter{} + path := field.NewPath("auditpolicy", "spec") + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + err := a.validateFields(c.Policy, path) + + if c.Expected == nil && err != nil { + assert.Failf(t, "received unexpected error '%s'", err.Error()) + } else if c.Expected != nil && err == nil { + assert.Failf(t, "expected to receive err '%s'", c.Expected.Error()) + } else if c.Expected != nil && err != nil { + assert.EqualError(t, err, c.Expected.Error()) + } + }) + } +}