k8s.io/kubernetes@v1.29.3/test/utils/audit.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package utils
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"io"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
    31  	auditinternal "k8s.io/apiserver/pkg/apis/audit"
    32  	"k8s.io/apiserver/pkg/audit"
    33  )
    34  
    35  // AuditEvent is a simplified representation of an audit event for testing purposes
    36  type AuditEvent struct {
    37  	ID                 types.UID
    38  	Level              auditinternal.Level
    39  	Stage              auditinternal.Stage
    40  	RequestURI         string
    41  	Verb               string
    42  	Code               int32
    43  	User               string
    44  	ImpersonatedUser   string
    45  	ImpersonatedGroups string
    46  	Resource           string
    47  	Namespace          string
    48  	RequestObject      bool
    49  	ResponseObject     bool
    50  	AuthorizeDecision  string
    51  
    52  	// The Check functions in this package takes ownerships of these maps. You should
    53  	// not reference these maps after calling the Check functions.
    54  	AdmissionWebhookMutationAnnotations map[string]string
    55  	AdmissionWebhookPatchAnnotations    map[string]string
    56  
    57  	// Only populated when a filter is provided to testEventFromInternalFiltered
    58  	CustomAuditAnnotations map[string]string
    59  }
    60  
    61  type AuditAnnotationsFilter func(key, val string) bool
    62  
    63  // MissingEventsReport provides an analysis if any events are missing
    64  type MissingEventsReport struct {
    65  	FirstEventChecked *auditinternal.Event
    66  	LastEventChecked  *auditinternal.Event
    67  	NumEventsChecked  int
    68  	MissingEvents     []AuditEvent
    69  }
    70  
    71  // String returns a human readable string representation of the report
    72  func (m *MissingEventsReport) String() string {
    73  	return fmt.Sprintf(`missing %d events
    74  
    75  - first event checked: %#v
    76  
    77  - last event checked: %#v
    78  
    79  - number of events checked: %d
    80  
    81  - missing events: %#v`, len(m.MissingEvents), m.FirstEventChecked, m.LastEventChecked, m.NumEventsChecked, m.MissingEvents)
    82  }
    83  
    84  // CheckAuditLines searches the audit log for the expected audit lines.
    85  func CheckAuditLines(stream io.Reader, expected []AuditEvent, version schema.GroupVersion) (missingReport *MissingEventsReport, err error) {
    86  	return CheckAuditLinesFiltered(stream, expected, version, nil)
    87  }
    88  
    89  // CheckAuditLinesFiltered searches the audit log for the expected audit lines, customAnnotationsFilter
    90  // controls which audit annotations are added to AuditEvent.CustomAuditAnnotations.
    91  // If the customAnnotationsFilter is nil, AuditEvent.CustomAuditAnnotations will be empty.
    92  func CheckAuditLinesFiltered(stream io.Reader, expected []AuditEvent, version schema.GroupVersion, customAnnotationsFilter AuditAnnotationsFilter) (missingReport *MissingEventsReport, err error) {
    93  	expectations := newAuditEventTracker(expected)
    94  
    95  	scanner := bufio.NewScanner(stream)
    96  
    97  	missingReport = &MissingEventsReport{
    98  		MissingEvents: expected,
    99  	}
   100  
   101  	var i int
   102  	for i = 0; scanner.Scan(); i++ {
   103  		line := scanner.Text()
   104  
   105  		e := &auditinternal.Event{}
   106  		decoder := audit.Codecs.UniversalDecoder(version)
   107  		if err := runtime.DecodeInto(decoder, []byte(line), e); err != nil {
   108  			return missingReport, fmt.Errorf("failed decoding buf: %s, apiVersion: %s", line, version)
   109  		}
   110  		if i == 0 {
   111  			missingReport.FirstEventChecked = e
   112  		}
   113  		missingReport.LastEventChecked = e
   114  
   115  		event, err := testEventFromInternalFiltered(e, customAnnotationsFilter)
   116  		if err != nil {
   117  			return missingReport, err
   118  		}
   119  
   120  		expectations.Mark(event)
   121  	}
   122  	if err := scanner.Err(); err != nil {
   123  		return missingReport, err
   124  	}
   125  
   126  	missingReport.MissingEvents = expectations.Missing()
   127  	missingReport.NumEventsChecked = i
   128  	return missingReport, nil
   129  }
   130  
   131  // CheckAuditList searches an audit event list for the expected audit events.
   132  func CheckAuditList(el auditinternal.EventList, expected []AuditEvent) (missing []AuditEvent, err error) {
   133  	expectations := newAuditEventTracker(expected)
   134  
   135  	for _, e := range el.Items {
   136  		event, err := testEventFromInternal(&e)
   137  		if err != nil {
   138  			return expected, err
   139  		}
   140  
   141  		expectations.Mark(event)
   142  	}
   143  
   144  	return expectations.Missing(), nil
   145  }
   146  
   147  // CheckForDuplicates checks a list for duplicate events
   148  func CheckForDuplicates(el auditinternal.EventList) (auditinternal.EventList, error) {
   149  	// existingEvents holds a slice of audit events that have been seen
   150  	existingEvents := []AuditEvent{}
   151  	duplicates := auditinternal.EventList{}
   152  	for _, e := range el.Items {
   153  		event, err := testEventFromInternal(&e)
   154  		if err != nil {
   155  			return duplicates, err
   156  		}
   157  		event.ID = e.AuditID
   158  		for _, existing := range existingEvents {
   159  			if reflect.DeepEqual(existing, event) {
   160  				duplicates.Items = append(duplicates.Items, e)
   161  				continue
   162  			}
   163  		}
   164  		existingEvents = append(existingEvents, event)
   165  	}
   166  
   167  	var err error
   168  	if len(duplicates.Items) > 0 {
   169  		err = fmt.Errorf("failed duplicate check")
   170  	}
   171  
   172  	return duplicates, err
   173  }
   174  
   175  // testEventFromInternal takes an internal audit event and returns a test event
   176  func testEventFromInternal(e *auditinternal.Event) (AuditEvent, error) {
   177  	return testEventFromInternalFiltered(e, nil)
   178  }
   179  
   180  // testEventFromInternalFiltered takes an internal audit event and returns a test event, customAnnotationsFilter
   181  // controls which audit annotations are added to AuditEvent.CustomAuditAnnotations.
   182  // If the customAnnotationsFilter is nil, AuditEvent.CustomAuditAnnotations will be empty.
   183  func testEventFromInternalFiltered(e *auditinternal.Event, customAnnotationsFilter AuditAnnotationsFilter) (AuditEvent, error) {
   184  	event := AuditEvent{
   185  		Level:      e.Level,
   186  		Stage:      e.Stage,
   187  		RequestURI: e.RequestURI,
   188  		Verb:       e.Verb,
   189  		User:       e.User.Username,
   190  	}
   191  	if e.ObjectRef != nil {
   192  		event.Namespace = e.ObjectRef.Namespace
   193  		event.Resource = e.ObjectRef.Resource
   194  	}
   195  	if e.ResponseStatus != nil {
   196  		event.Code = e.ResponseStatus.Code
   197  	}
   198  	if e.ResponseObject != nil {
   199  		event.ResponseObject = true
   200  	}
   201  	if e.RequestObject != nil {
   202  		event.RequestObject = true
   203  	}
   204  	if e.ImpersonatedUser != nil {
   205  		event.ImpersonatedUser = e.ImpersonatedUser.Username
   206  		sort.Strings(e.ImpersonatedUser.Groups)
   207  		event.ImpersonatedGroups = strings.Join(e.ImpersonatedUser.Groups, ",")
   208  	}
   209  	event.AuthorizeDecision = e.Annotations["authorization.k8s.io/decision"]
   210  	for k, v := range e.Annotations {
   211  		if strings.HasPrefix(k, mutating.PatchAuditAnnotationPrefix) {
   212  			if event.AdmissionWebhookPatchAnnotations == nil {
   213  				event.AdmissionWebhookPatchAnnotations = map[string]string{}
   214  			}
   215  			event.AdmissionWebhookPatchAnnotations[k] = v
   216  		} else if strings.HasPrefix(k, mutating.MutationAuditAnnotationPrefix) {
   217  			if event.AdmissionWebhookMutationAnnotations == nil {
   218  				event.AdmissionWebhookMutationAnnotations = map[string]string{}
   219  			}
   220  			event.AdmissionWebhookMutationAnnotations[k] = v
   221  		} else if customAnnotationsFilter != nil && customAnnotationsFilter(k, v) {
   222  			if event.CustomAuditAnnotations == nil {
   223  				event.CustomAuditAnnotations = map[string]string{}
   224  			}
   225  			event.CustomAuditAnnotations[k] = v
   226  		}
   227  	}
   228  	return event, nil
   229  }
   230  
   231  // auditEvent is a private wrapper on top of AuditEvent used by auditEventTracker
   232  type auditEvent struct {
   233  	event AuditEvent
   234  	found bool
   235  }
   236  
   237  // auditEventTracker keeps track of AuditEvent expectations and marks matching events as found
   238  type auditEventTracker struct {
   239  	events []*auditEvent
   240  }
   241  
   242  // newAuditEventTracker creates a tracker that tracks whether expect events are found
   243  func newAuditEventTracker(expected []AuditEvent) *auditEventTracker {
   244  	expectations := &auditEventTracker{events: []*auditEvent{}}
   245  	for _, event := range expected {
   246  		// we copy the references to the maps in event
   247  		expectations.events = append(expectations.events, &auditEvent{event: event, found: false})
   248  	}
   249  	return expectations
   250  }
   251  
   252  // Mark marks the given event as found if it's expected
   253  func (t *auditEventTracker) Mark(event AuditEvent) {
   254  	for _, e := range t.events {
   255  		if reflect.DeepEqual(e.event, event) {
   256  			e.found = true
   257  		}
   258  	}
   259  }
   260  
   261  // Missing reports events that are expected but not found
   262  func (t *auditEventTracker) Missing() []AuditEvent {
   263  	var missing []AuditEvent
   264  	for _, e := range t.events {
   265  		if !e.found {
   266  			missing = append(missing, e.event)
   267  		}
   268  	}
   269  	return missing
   270  }