mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-13 10:33:35 +00:00
build: move e2e dependencies into e2e/go.mod
Several packages are only used while running the e2e suite. These packages are less important to update, as the they can not influence the final executable that is part of the Ceph-CSI container-image. By moving these dependencies out of the main Ceph-CSI go.mod, it is easier to identify if a reported CVE affects Ceph-CSI, or only the testing (like most of the Kubernetes CVEs). Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
committed by
mergify[bot]
parent
15da101b1b
commit
bec6090996
290
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/buffered/buffered.go
generated
vendored
Normal file
290
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/buffered/buffered.go
generated
vendored
Normal file
@ -0,0 +1,290 @@
|
||||
/*
|
||||
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 buffered
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
)
|
||||
|
||||
// PluginName is the name reported in error metrics.
|
||||
const PluginName = "buffered"
|
||||
|
||||
// BatchConfig represents batching delegate audit backend configuration.
|
||||
type BatchConfig struct {
|
||||
// BufferSize defines a size of the buffering queue.
|
||||
BufferSize int
|
||||
// MaxBatchSize defines maximum size of a batch.
|
||||
MaxBatchSize int
|
||||
// MaxBatchWait indicates the maximum interval between two batches.
|
||||
MaxBatchWait time.Duration
|
||||
|
||||
// ThrottleEnable defines whether throttling will be applied to the batching process.
|
||||
ThrottleEnable bool
|
||||
// ThrottleQPS defines the allowed rate of batches per second sent to the delegate backend.
|
||||
ThrottleQPS float32
|
||||
// ThrottleBurst defines the maximum number of requests sent to the delegate backend at the same moment in case
|
||||
// the capacity defined by ThrottleQPS was not utilized.
|
||||
ThrottleBurst int
|
||||
|
||||
// Whether the delegate backend should be called asynchronously.
|
||||
AsyncDelegate bool
|
||||
}
|
||||
|
||||
type bufferedBackend struct {
|
||||
// The delegate backend that actually exports events.
|
||||
delegateBackend audit.Backend
|
||||
|
||||
// Channel to buffer events before sending to the delegate backend.
|
||||
buffer chan *auditinternal.Event
|
||||
// Maximum number of events in a batch sent to the delegate backend.
|
||||
maxBatchSize int
|
||||
// Amount of time to wait after sending a batch to the delegate backend before sending another one.
|
||||
//
|
||||
// Receiving maxBatchSize events will always trigger sending a batch, regardless of the amount of time passed.
|
||||
maxBatchWait time.Duration
|
||||
|
||||
// Whether the delegate backend should be called asynchronously.
|
||||
asyncDelegate bool
|
||||
|
||||
// Channel to signal that the batching routine has processed all remaining events and exited.
|
||||
// Once `shutdownCh` is closed no new events will be sent to the delegate backend.
|
||||
shutdownCh chan struct{}
|
||||
|
||||
// WaitGroup to control the concurrency of sending batches to the delegate backend.
|
||||
// Worker routine calls Add before sending a batch and
|
||||
// then spawns a routine that calls Done after batch was processed by the delegate backend.
|
||||
// This WaitGroup is used to wait for all sending routines to finish before shutting down audit backend.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Limits the number of batches sent to the delegate backend per second.
|
||||
throttle flowcontrol.RateLimiter
|
||||
}
|
||||
|
||||
var _ audit.Backend = &bufferedBackend{}
|
||||
|
||||
// NewBackend returns a buffered audit backend that wraps delegate backend.
|
||||
// Buffered backend automatically runs and shuts down the delegate backend.
|
||||
func NewBackend(delegate audit.Backend, config BatchConfig) audit.Backend {
|
||||
var throttle flowcontrol.RateLimiter
|
||||
if config.ThrottleEnable {
|
||||
throttle = flowcontrol.NewTokenBucketRateLimiter(config.ThrottleQPS, config.ThrottleBurst)
|
||||
}
|
||||
return &bufferedBackend{
|
||||
delegateBackend: delegate,
|
||||
buffer: make(chan *auditinternal.Event, config.BufferSize),
|
||||
maxBatchSize: config.MaxBatchSize,
|
||||
maxBatchWait: config.MaxBatchWait,
|
||||
asyncDelegate: config.AsyncDelegate,
|
||||
shutdownCh: make(chan struct{}),
|
||||
wg: sync.WaitGroup{},
|
||||
throttle: throttle,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bufferedBackend) Run(stopCh <-chan struct{}) error {
|
||||
go func() {
|
||||
// Signal that the working routine has exited.
|
||||
defer close(b.shutdownCh)
|
||||
|
||||
b.processIncomingEvents(stopCh)
|
||||
|
||||
// Handle the events that were received after the last buffer
|
||||
// scraping and before this line. Since the buffer is closed, no new
|
||||
// events will come through.
|
||||
allEventsProcessed := false
|
||||
timer := make(chan time.Time)
|
||||
for !allEventsProcessed {
|
||||
allEventsProcessed = func() bool {
|
||||
// Recover from any panic in order to try to process all remaining events.
|
||||
// Note, that in case of a panic, the return value will be false and
|
||||
// the loop execution will continue.
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
events := b.collectEvents(timer, wait.NeverStop)
|
||||
b.processEvents(events)
|
||||
return len(events) == 0
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return b.delegateBackend.Run(stopCh)
|
||||
}
|
||||
|
||||
// Shutdown blocks until stopCh passed to the Run method is closed and all
|
||||
// events added prior to that moment are batched and sent to the delegate backend.
|
||||
func (b *bufferedBackend) Shutdown() {
|
||||
// Wait until the routine spawned in Run method exits.
|
||||
<-b.shutdownCh
|
||||
|
||||
// Wait until all sending routines exit.
|
||||
//
|
||||
// - When b.shutdownCh is closed, we know that the goroutine in Run has terminated.
|
||||
// - This means that processIncomingEvents has terminated.
|
||||
// - Which means that b.buffer is closed and cannot accept any new events anymore.
|
||||
// - Because processEvents is called synchronously from the Run goroutine, the waitgroup has its final value.
|
||||
// Hence wg.Wait will not miss any more outgoing batches.
|
||||
b.wg.Wait()
|
||||
|
||||
b.delegateBackend.Shutdown()
|
||||
}
|
||||
|
||||
// processIncomingEvents runs a loop that collects events from the buffer. When
|
||||
// b.stopCh is closed, processIncomingEvents stops and closes the buffer.
|
||||
func (b *bufferedBackend) processIncomingEvents(stopCh <-chan struct{}) {
|
||||
defer close(b.buffer)
|
||||
|
||||
var (
|
||||
maxWaitChan <-chan time.Time
|
||||
maxWaitTimer *time.Timer
|
||||
)
|
||||
// Only use max wait batching if batching is enabled.
|
||||
if b.maxBatchSize > 1 {
|
||||
maxWaitTimer = time.NewTimer(b.maxBatchWait)
|
||||
maxWaitChan = maxWaitTimer.C
|
||||
defer maxWaitTimer.Stop()
|
||||
}
|
||||
|
||||
for {
|
||||
func() {
|
||||
// Recover from any panics caused by this function so a panic in the
|
||||
// goroutine can't bring down the main routine.
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
if b.maxBatchSize > 1 {
|
||||
maxWaitTimer.Reset(b.maxBatchWait)
|
||||
}
|
||||
b.processEvents(b.collectEvents(maxWaitChan, stopCh))
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectEvents attempts to collect some number of events in a batch.
|
||||
//
|
||||
// The following things can cause collectEvents to stop and return the list
|
||||
// of events:
|
||||
//
|
||||
// - Maximum number of events for a batch.
|
||||
// - Timer has passed.
|
||||
// - Buffer channel is closed and empty.
|
||||
// - stopCh is closed.
|
||||
func (b *bufferedBackend) collectEvents(timer <-chan time.Time, stopCh <-chan struct{}) []*auditinternal.Event {
|
||||
var events []*auditinternal.Event
|
||||
|
||||
L:
|
||||
for i := 0; i < b.maxBatchSize; i++ {
|
||||
select {
|
||||
case ev, ok := <-b.buffer:
|
||||
// Buffer channel was closed and no new events will follow.
|
||||
if !ok {
|
||||
break L
|
||||
}
|
||||
events = append(events, ev)
|
||||
case <-timer:
|
||||
// Timer has expired. Send currently accumulated batch.
|
||||
break L
|
||||
case <-stopCh:
|
||||
// Backend has been stopped. Send currently accumulated batch.
|
||||
break L
|
||||
}
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// processEvents process the batch events in a goroutine using delegateBackend's ProcessEvents.
|
||||
func (b *bufferedBackend) processEvents(events []*auditinternal.Event) {
|
||||
if len(events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(audit): Should control the number of active goroutines
|
||||
// if one goroutine takes 5 seconds to finish, the number of goroutines can be 5 * defaultBatchThrottleQPS
|
||||
if b.throttle != nil {
|
||||
b.throttle.Accept()
|
||||
}
|
||||
|
||||
if b.asyncDelegate {
|
||||
b.wg.Add(1)
|
||||
go func() {
|
||||
defer b.wg.Done()
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
// Execute the real processing in a goroutine to keep it from blocking.
|
||||
// This lets the batching routine continue draining the queue immediately.
|
||||
b.delegateBackend.ProcessEvents(events...)
|
||||
}()
|
||||
} else {
|
||||
func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
// Execute the real processing in a goroutine to keep it from blocking.
|
||||
// This lets the batching routine continue draining the queue immediately.
|
||||
b.delegateBackend.ProcessEvents(events...)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bufferedBackend) ProcessEvents(ev ...*auditinternal.Event) bool {
|
||||
// The following mechanism is in place to support the situation when audit
|
||||
// events are still coming after the backend was stopped.
|
||||
var sendErr error
|
||||
var evIndex int
|
||||
|
||||
// If the delegateBackend was shutdown and the buffer channel was closed, an
|
||||
// attempt to add an event to it will result in panic that we should
|
||||
// recover from.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
sendErr = fmt.Errorf("audit backend shut down")
|
||||
}
|
||||
if sendErr != nil {
|
||||
audit.HandlePluginError(PluginName, sendErr, ev[evIndex:]...)
|
||||
}
|
||||
}()
|
||||
|
||||
for i, e := range ev {
|
||||
evIndex = i
|
||||
// Per the audit.Backend interface these events are reused after being
|
||||
// sent to the Sink. Deep copy and send the copy to the queue.
|
||||
event := e.DeepCopy()
|
||||
|
||||
select {
|
||||
case b.buffer <- event:
|
||||
default:
|
||||
sendErr = fmt.Errorf("audit buffer queue blocked")
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *bufferedBackend) String() string {
|
||||
return fmt.Sprintf("%s<%s>", PluginName, b.delegateBackend)
|
||||
}
|
19
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/buffered/doc.go
generated
vendored
Normal file
19
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/buffered/doc.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
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 buffered provides an implementation for the audit.Backend interface
|
||||
// that batches incoming audit events and sends batches to the delegate audit.Backend.
|
||||
package buffered // import "k8s.io/apiserver/plugin/pkg/audit/buffered"
|
104
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/log/backend.go
generated
vendored
Normal file
104
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/log/backend.go
generated
vendored
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2017 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 log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatLegacy saves event in 1-line text format.
|
||||
FormatLegacy = "legacy"
|
||||
// FormatJson saves event in structured json format.
|
||||
FormatJson = "json"
|
||||
|
||||
// PluginName is the name of this plugin, to be used in help and logs.
|
||||
PluginName = "log"
|
||||
)
|
||||
|
||||
// AllowedFormats are the formats known by log backend.
|
||||
var AllowedFormats = []string{
|
||||
FormatLegacy,
|
||||
FormatJson,
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
out io.Writer
|
||||
format string
|
||||
encoder runtime.Encoder
|
||||
}
|
||||
|
||||
var _ audit.Backend = &backend{}
|
||||
|
||||
func NewBackend(out io.Writer, format string, groupVersion schema.GroupVersion) audit.Backend {
|
||||
return &backend{
|
||||
out: out,
|
||||
format: format,
|
||||
encoder: audit.Codecs.LegacyCodec(groupVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) ProcessEvents(events ...*auditinternal.Event) bool {
|
||||
success := true
|
||||
for _, ev := range events {
|
||||
success = b.logEvent(ev) && success
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
func (b *backend) logEvent(ev *auditinternal.Event) bool {
|
||||
line := ""
|
||||
switch b.format {
|
||||
case FormatLegacy:
|
||||
line = audit.EventString(ev) + "\n"
|
||||
case FormatJson:
|
||||
bs, err := runtime.Encode(b.encoder, ev)
|
||||
if err != nil {
|
||||
audit.HandlePluginError(PluginName, err, ev)
|
||||
return false
|
||||
}
|
||||
line = string(bs[:])
|
||||
default:
|
||||
audit.HandlePluginError(PluginName, fmt.Errorf("log format %q is not in list of known formats (%s)",
|
||||
b.format, strings.Join(AllowedFormats, ",")), ev)
|
||||
return false
|
||||
}
|
||||
if _, err := fmt.Fprint(b.out, line); err != nil {
|
||||
audit.HandlePluginError(PluginName, err, ev)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *backend) Run(stopCh <-chan struct{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *backend) Shutdown() {
|
||||
// Nothing to do here.
|
||||
}
|
||||
|
||||
func (b *backend) String() string {
|
||||
return PluginName
|
||||
}
|
19
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/truncate/doc.go
generated
vendored
Normal file
19
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/truncate/doc.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
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 truncate provides an implementation for the audit.Backend interface
|
||||
// that truncates audit events and sends them to the delegate audit.Backend.
|
||||
package truncate // import "k8s.io/apiserver/plugin/pkg/audit/truncate"
|
165
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/truncate/truncate.go
generated
vendored
Normal file
165
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/truncate/truncate.go
generated
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
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 truncate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName is the name reported in error metrics.
|
||||
PluginName = "truncate"
|
||||
|
||||
// annotationKey defines the name of the annotation used to indicate truncation.
|
||||
annotationKey = "audit.k8s.io/truncated"
|
||||
// annotationValue defines the value of the annotation used to indicate truncation.
|
||||
annotationValue = "true"
|
||||
)
|
||||
|
||||
// Config represents truncating backend configuration.
|
||||
type Config struct {
|
||||
// MaxEventSize defines max allowed size of the event. If the event is larger,
|
||||
// truncating will be performed.
|
||||
MaxEventSize int64
|
||||
|
||||
// MaxBatchSize defined max allowed size of the batch of events, passed to the backend.
|
||||
// If the total size of the batch is larger than this number, batch will be split. Actual
|
||||
// size of the serialized request might be slightly higher, on the order of hundreds of bytes.
|
||||
MaxBatchSize int64
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
// The delegate backend that actually exports events.
|
||||
delegateBackend audit.Backend
|
||||
|
||||
// Configuration used for truncation.
|
||||
c Config
|
||||
|
||||
// Encoder used to calculate audit event sizes.
|
||||
e runtime.Encoder
|
||||
}
|
||||
|
||||
var _ audit.Backend = &backend{}
|
||||
|
||||
// NewBackend returns a new truncating backend, using configuration passed in the parameters.
|
||||
// Truncate backend automatically runs and shut downs the delegate backend.
|
||||
func NewBackend(delegateBackend audit.Backend, config Config, groupVersion schema.GroupVersion) audit.Backend {
|
||||
return &backend{
|
||||
delegateBackend: delegateBackend,
|
||||
c: config,
|
||||
e: audit.Codecs.LegacyCodec(groupVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) ProcessEvents(events ...*auditinternal.Event) bool {
|
||||
var errors []error
|
||||
var impacted []*auditinternal.Event
|
||||
var batch []*auditinternal.Event
|
||||
var batchSize int64
|
||||
success := true
|
||||
for _, event := range events {
|
||||
size, err := b.calcSize(event)
|
||||
// If event was correctly serialized, but the size is more than allowed
|
||||
// and it makes sense to do trimming, i.e. there's a request and/or
|
||||
// response present, try to strip away request and response.
|
||||
if err == nil && size > b.c.MaxEventSize && event.Level.GreaterOrEqual(auditinternal.LevelRequest) {
|
||||
event = truncate(event)
|
||||
size, err = b.calcSize(event)
|
||||
}
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
impacted = append(impacted, event)
|
||||
continue
|
||||
}
|
||||
if size > b.c.MaxEventSize {
|
||||
errors = append(errors, fmt.Errorf("event is too large even after truncating"))
|
||||
impacted = append(impacted, event)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(batch) > 0 && batchSize+size > b.c.MaxBatchSize {
|
||||
success = b.delegateBackend.ProcessEvents(batch...) && success
|
||||
batch = []*auditinternal.Event{}
|
||||
batchSize = 0
|
||||
}
|
||||
|
||||
batchSize += size
|
||||
batch = append(batch, event)
|
||||
}
|
||||
|
||||
if len(batch) > 0 {
|
||||
success = b.delegateBackend.ProcessEvents(batch...) && success
|
||||
}
|
||||
|
||||
if len(impacted) > 0 {
|
||||
audit.HandlePluginError(PluginName, utilerrors.NewAggregate(errors), impacted...)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// truncate removed request and response objects from the audit events,
|
||||
// to try and keep at least metadata.
|
||||
func truncate(e *auditinternal.Event) *auditinternal.Event {
|
||||
// Make a shallow copy to avoid copying response/request objects.
|
||||
newEvent := &auditinternal.Event{}
|
||||
*newEvent = *e
|
||||
|
||||
newEvent.RequestObject = nil
|
||||
newEvent.ResponseObject = nil
|
||||
|
||||
if newEvent.Annotations == nil {
|
||||
newEvent.Annotations = make(map[string]string)
|
||||
}
|
||||
newEvent.Annotations[annotationKey] = annotationValue
|
||||
|
||||
return newEvent
|
||||
}
|
||||
|
||||
func (b *backend) Run(stopCh <-chan struct{}) error {
|
||||
return b.delegateBackend.Run(stopCh)
|
||||
}
|
||||
|
||||
func (b *backend) Shutdown() {
|
||||
b.delegateBackend.Shutdown()
|
||||
}
|
||||
|
||||
func (b *backend) calcSize(e *auditinternal.Event) (int64, error) {
|
||||
s := &sizer{}
|
||||
if err := b.e.Encode(e, s); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.Size, nil
|
||||
}
|
||||
|
||||
func (b *backend) String() string {
|
||||
return fmt.Sprintf("%s<%s>", PluginName, b.delegateBackend)
|
||||
}
|
||||
|
||||
type sizer struct {
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (s *sizer) Write(p []byte) (n int, err error) {
|
||||
s.Size += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
146
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/webhook/webhook.go
generated
vendored
Normal file
146
e2e/vendor/k8s.io/apiserver/plugin/pkg/audit/webhook/webhook.go
generated
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2017 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 webhook implements the audit.Backend interface using HTTP webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/apis/audit/install"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/component-base/tracing"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName is the name of this plugin, to be used in help and logs.
|
||||
PluginName = "webhook"
|
||||
|
||||
// DefaultInitialBackoffDelay is the default amount of time to wait before
|
||||
// retrying sending audit events through a webhook.
|
||||
DefaultInitialBackoffDelay = 10 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(audit.Scheme)
|
||||
}
|
||||
|
||||
// retryOnError enforces the webhook client to retry requests
|
||||
// on error regardless of its nature.
|
||||
// The default implementation considers a very limited set of
|
||||
// 'retriable' errors, assuming correct use of HTTP codes by
|
||||
// external webhooks.
|
||||
// That may easily lead to dropped audit events. In fact, there is
|
||||
// hardly any error that could be a justified reason NOT to retry
|
||||
// sending audit events if there is even a slight chance that the
|
||||
// receiving service gets back to normal at some point.
|
||||
func retryOnError(err error) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadWebhook(configFile string, groupVersion schema.GroupVersion, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (*webhook.GenericWebhook, error) {
|
||||
clientConfig, err := webhook.LoadKubeconfig(configFile, customDial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := webhook.NewGenericWebhook(audit.Scheme, audit.Codecs, clientConfig,
|
||||
[]schema.GroupVersion{groupVersion}, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.ShouldRetry = retryOnError
|
||||
return w, nil
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
w *webhook.GenericWebhook
|
||||
name string
|
||||
}
|
||||
|
||||
// NewDynamicBackend returns an audit backend configured from a REST client that
|
||||
// sends events over HTTP to an external service.
|
||||
func NewDynamicBackend(rc *rest.RESTClient, retryBackoff wait.Backoff) audit.Backend {
|
||||
return &backend{
|
||||
w: &webhook.GenericWebhook{
|
||||
RestClient: rc,
|
||||
RetryBackoff: retryBackoff,
|
||||
ShouldRetry: retryOnError,
|
||||
},
|
||||
name: fmt.Sprintf("dynamic_%s", PluginName),
|
||||
}
|
||||
}
|
||||
|
||||
// NewBackend returns an audit backend that sends events over HTTP to an external service.
|
||||
func NewBackend(kubeConfigFile string, groupVersion schema.GroupVersion, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (audit.Backend, error) {
|
||||
w, err := loadWebhook(kubeConfigFile, groupVersion, retryBackoff, customDial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &backend{w: w, name: PluginName}, nil
|
||||
}
|
||||
|
||||
func (b *backend) Run(stopCh <-chan struct{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *backend) Shutdown() {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
func (b *backend) ProcessEvents(ev ...*auditinternal.Event) bool {
|
||||
if err := b.processEvents(ev...); err != nil {
|
||||
audit.HandlePluginError(b.String(), err, ev...)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *backend) processEvents(ev ...*auditinternal.Event) error {
|
||||
var list auditinternal.EventList
|
||||
for _, e := range ev {
|
||||
list.Items = append(list.Items, *e)
|
||||
}
|
||||
return b.w.WithExponentialBackoff(context.Background(), func() rest.Result {
|
||||
ctx, span := tracing.Start(context.Background(), "Call Audit Events webhook",
|
||||
attribute.String("name", b.name),
|
||||
attribute.Int("event-count", len(list.Items)),
|
||||
)
|
||||
// Only log audit webhook traces that exceed a 25ms per object limit plus a 50ms
|
||||
// request overhead allowance. The high per object limit used here is primarily to
|
||||
// allow enough time for the serialization/deserialization of audit events, which
|
||||
// contain nested request and response objects plus additional event fields.
|
||||
defer span.End(time.Duration(50+25*len(list.Items)) * time.Millisecond)
|
||||
return b.w.RestClient.Post().Body(&list).Do(ctx)
|
||||
}).Error()
|
||||
}
|
||||
|
||||
func (b *backend) String() string {
|
||||
return b.name
|
||||
}
|
35
e2e/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/metrics.go
generated
vendored
Normal file
35
e2e/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/metrics.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2021 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 webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// AuthenticatorMetrics specifies a set of methods that are used to register various metrics
|
||||
type AuthenticatorMetrics struct {
|
||||
// RecordRequestTotal increments the total number of requests for webhooks
|
||||
RecordRequestTotal func(ctx context.Context, code string)
|
||||
|
||||
// RecordRequestLatency measures request latency in seconds for webhooks. Broken down by status code.
|
||||
RecordRequestLatency func(ctx context.Context, code string, latency float64)
|
||||
}
|
||||
|
||||
type noopMetrics struct{}
|
||||
|
||||
func (noopMetrics) RequestTotal(context.Context, string) {}
|
||||
func (noopMetrics) RequestLatency(context.Context, string, float64) {}
|
327
e2e/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go
generated
vendored
Normal file
327
e2e/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go
generated
vendored
Normal file
@ -0,0 +1,327 @@
|
||||
/*
|
||||
Copyright 2016 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 webhook implements the authenticator.Token interface using HTTP webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// DefaultRetryBackoff returns the default backoff parameters for webhook retry.
|
||||
func DefaultRetryBackoff() *wait.Backoff {
|
||||
backoff := webhook.DefaultRetryBackoffWithInitialDelay(500 * time.Millisecond)
|
||||
return &backoff
|
||||
}
|
||||
|
||||
// Ensure WebhookTokenAuthenticator implements the authenticator.Token interface.
|
||||
var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
|
||||
|
||||
type tokenReviewer interface {
|
||||
Create(ctx context.Context, review *authenticationv1.TokenReview, _ metav1.CreateOptions) (*authenticationv1.TokenReview, int, error)
|
||||
}
|
||||
|
||||
type WebhookTokenAuthenticator struct {
|
||||
tokenReview tokenReviewer
|
||||
retryBackoff wait.Backoff
|
||||
implicitAuds authenticator.Audiences
|
||||
requestTimeout time.Duration
|
||||
metrics AuthenticatorMetrics
|
||||
}
|
||||
|
||||
// NewFromInterface creates a webhook authenticator using the given tokenReview
|
||||
// client. It is recommend to wrap this authenticator with the token cache
|
||||
// authenticator implemented in
|
||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||
func NewFromInterface(tokenReview authenticationv1client.AuthenticationV1Interface, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff, requestTimeout time.Duration, metrics AuthenticatorMetrics) (*WebhookTokenAuthenticator, error) {
|
||||
tokenReviewClient := &tokenReviewV1Client{tokenReview.RESTClient()}
|
||||
return newWithBackoff(tokenReviewClient, retryBackoff, implicitAuds, requestTimeout, metrics)
|
||||
}
|
||||
|
||||
// New creates a new WebhookTokenAuthenticator from the provided rest
|
||||
// config. It is recommend to wrap this authenticator with the token cache
|
||||
// authenticator implemented in
|
||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||
func New(config *rest.Config, version string, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff) (*WebhookTokenAuthenticator, error) {
|
||||
tokenReview, err := tokenReviewInterfaceFromConfig(config, version, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWithBackoff(tokenReview, retryBackoff, implicitAuds, time.Duration(0), AuthenticatorMetrics{
|
||||
RecordRequestTotal: noopMetrics{}.RequestTotal,
|
||||
RecordRequestLatency: noopMetrics{}.RequestLatency,
|
||||
})
|
||||
}
|
||||
|
||||
// newWithBackoff allows tests to skip the sleep.
|
||||
func newWithBackoff(tokenReview tokenReviewer, retryBackoff wait.Backoff, implicitAuds authenticator.Audiences, requestTimeout time.Duration, metrics AuthenticatorMetrics) (*WebhookTokenAuthenticator, error) {
|
||||
return &WebhookTokenAuthenticator{
|
||||
tokenReview,
|
||||
retryBackoff,
|
||||
implicitAuds,
|
||||
requestTimeout,
|
||||
metrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthenticateToken implements the authenticator.Token interface.
|
||||
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
// We take implicit audiences of the API server at WebhookTokenAuthenticator
|
||||
// construction time. The outline of how we validate audience here is:
|
||||
//
|
||||
// * if the ctx is not audience limited, don't do any audience validation.
|
||||
// * if ctx is audience-limited, add the audiences to the tokenreview spec
|
||||
// * if the tokenreview returns with audiences in the status that intersect
|
||||
// with the audiences in the ctx, copy into the response and return success
|
||||
// * if the tokenreview returns without an audience in the status, ensure
|
||||
// the ctx audiences intersect with the implicit audiences, and set the
|
||||
// intersection in the response.
|
||||
// * otherwise return unauthenticated.
|
||||
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
|
||||
r := &authenticationv1.TokenReview{
|
||||
Spec: authenticationv1.TokenReviewSpec{
|
||||
Token: token,
|
||||
Audiences: wantAuds,
|
||||
},
|
||||
}
|
||||
var (
|
||||
result *authenticationv1.TokenReview
|
||||
auds authenticator.Audiences
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
// set a hard timeout if it was defined
|
||||
// if the child has a shorter deadline then it will expire first,
|
||||
// otherwise if the parent has a shorter deadline then the parent will expire and it will be propagate to the child
|
||||
if w.requestTimeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, w.requestTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// WithExponentialBackoff will return tokenreview create error (tokenReviewErr) if any.
|
||||
if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
|
||||
var tokenReviewErr error
|
||||
var statusCode int
|
||||
|
||||
start := time.Now()
|
||||
result, statusCode, tokenReviewErr = w.tokenReview.Create(ctx, r, metav1.CreateOptions{})
|
||||
latency := time.Since(start)
|
||||
|
||||
if statusCode != 0 {
|
||||
w.metrics.RecordRequestTotal(ctx, strconv.Itoa(statusCode))
|
||||
w.metrics.RecordRequestLatency(ctx, strconv.Itoa(statusCode), latency.Seconds())
|
||||
return tokenReviewErr
|
||||
}
|
||||
|
||||
if tokenReviewErr != nil {
|
||||
w.metrics.RecordRequestTotal(ctx, "<error>")
|
||||
w.metrics.RecordRequestLatency(ctx, "<error>", latency.Seconds())
|
||||
}
|
||||
return tokenReviewErr
|
||||
}, webhook.DefaultShouldRetry); err != nil {
|
||||
// An error here indicates bad configuration or an outage. Log for debugging.
|
||||
klog.Errorf("Failed to make webhook authenticator request: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if checkAuds {
|
||||
gotAuds := w.implicitAuds
|
||||
if len(result.Status.Audiences) > 0 {
|
||||
gotAuds = result.Status.Audiences
|
||||
}
|
||||
auds = wantAuds.Intersect(gotAuds)
|
||||
if len(auds) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
r.Status = result.Status
|
||||
if !r.Status.Authenticated {
|
||||
var err error
|
||||
if len(r.Status.Error) != 0 {
|
||||
err = errors.New(r.Status.Error)
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var extra map[string][]string
|
||||
if r.Status.User.Extra != nil {
|
||||
extra = map[string][]string{}
|
||||
for k, v := range r.Status.User.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: r.Status.User.Username,
|
||||
UID: r.Status.User.UID,
|
||||
Groups: r.Status.User.Groups,
|
||||
Extra: extra,
|
||||
},
|
||||
Audiences: auds,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// tokenReviewInterfaceFromConfig builds a client from the specified kubeconfig file,
|
||||
// and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview
|
||||
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
||||
func tokenReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (tokenReviewer, error) {
|
||||
localScheme := runtime.NewScheme()
|
||||
if err := scheme.AddToScheme(localScheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch version {
|
||||
case authenticationv1.SchemeGroupVersion.Version:
|
||||
groupVersions := []schema.GroupVersion{authenticationv1.SchemeGroupVersion}
|
||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenReviewV1ClientGW{gw.RestClient}, nil
|
||||
|
||||
case authenticationv1beta1.SchemeGroupVersion.Version:
|
||||
groupVersions := []schema.GroupVersion{authenticationv1beta1.SchemeGroupVersion}
|
||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenReviewV1beta1ClientGW{gw.RestClient}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported authentication webhook version %q, supported versions are %q, %q",
|
||||
version,
|
||||
authenticationv1.SchemeGroupVersion.Version,
|
||||
authenticationv1beta1.SchemeGroupVersion.Version,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type tokenReviewV1Client struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
// Create takes the representation of a tokenReview and creates it. Returns the server's representation of the tokenReview, HTTP status code and an error, if there is any.
|
||||
func (c *tokenReviewV1Client) Create(ctx context.Context, tokenReview *authenticationv1.TokenReview, opts metav1.CreateOptions) (result *authenticationv1.TokenReview, statusCode int, err error) {
|
||||
result = &authenticationv1.TokenReview{}
|
||||
|
||||
restResult := c.client.Post().
|
||||
Resource("tokenreviews").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Body(tokenReview).
|
||||
Do(ctx)
|
||||
|
||||
restResult.StatusCode(&statusCode)
|
||||
err = restResult.Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// tokenReviewV1ClientGW used by the generic webhook, doesn't specify GVR.
|
||||
type tokenReviewV1ClientGW struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
// Create takes the representation of a tokenReview and creates it. Returns the server's representation of the tokenReview, HTTP status code and an error, if there is any.
|
||||
func (c *tokenReviewV1ClientGW) Create(ctx context.Context, tokenReview *authenticationv1.TokenReview, opts metav1.CreateOptions) (result *authenticationv1.TokenReview, statusCode int, err error) {
|
||||
result = &authenticationv1.TokenReview{}
|
||||
|
||||
restResult := c.client.Post().
|
||||
Body(tokenReview).
|
||||
Do(ctx)
|
||||
|
||||
restResult.StatusCode(&statusCode)
|
||||
err = restResult.Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// tokenReviewV1beta1ClientGW used by the generic webhook, doesn't specify GVR.
|
||||
type tokenReviewV1beta1ClientGW struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
func (t *tokenReviewV1beta1ClientGW) Create(ctx context.Context, review *authenticationv1.TokenReview, _ metav1.CreateOptions) (*authenticationv1.TokenReview, int, error) {
|
||||
var statusCode int
|
||||
v1beta1Review := &authenticationv1beta1.TokenReview{Spec: v1SpecToV1beta1Spec(&review.Spec)}
|
||||
v1beta1Result := &authenticationv1beta1.TokenReview{}
|
||||
|
||||
restResult := t.client.Post().Body(v1beta1Review).Do(ctx)
|
||||
restResult.StatusCode(&statusCode)
|
||||
err := restResult.Into(v1beta1Result)
|
||||
if err != nil {
|
||||
return nil, statusCode, err
|
||||
}
|
||||
review.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
|
||||
return review, statusCode, nil
|
||||
}
|
||||
|
||||
func v1SpecToV1beta1Spec(in *authenticationv1.TokenReviewSpec) authenticationv1beta1.TokenReviewSpec {
|
||||
return authenticationv1beta1.TokenReviewSpec{
|
||||
Token: in.Token,
|
||||
Audiences: in.Audiences,
|
||||
}
|
||||
}
|
||||
|
||||
func v1beta1StatusToV1Status(in *authenticationv1beta1.TokenReviewStatus) authenticationv1.TokenReviewStatus {
|
||||
return authenticationv1.TokenReviewStatus{
|
||||
Authenticated: in.Authenticated,
|
||||
User: v1beta1UserToV1User(in.User),
|
||||
Audiences: in.Audiences,
|
||||
Error: in.Error,
|
||||
}
|
||||
}
|
||||
|
||||
func v1beta1UserToV1User(u authenticationv1beta1.UserInfo) authenticationv1.UserInfo {
|
||||
var extra map[string]authenticationv1.ExtraValue
|
||||
if u.Extra != nil {
|
||||
extra = make(map[string]authenticationv1.ExtraValue, len(u.Extra))
|
||||
for k, v := range u.Extra {
|
||||
extra[k] = authenticationv1.ExtraValue(v)
|
||||
}
|
||||
}
|
||||
return authenticationv1.UserInfo{
|
||||
Username: u.Username,
|
||||
UID: u.UID,
|
||||
Groups: u.Groups,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
102
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/gencerts.sh
generated
vendored
Normal file
102
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/gencerts.sh
generated
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2016 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.
|
||||
|
||||
set -e
|
||||
|
||||
# gencerts.sh generates the certificates for the webhook authz plugin tests.
|
||||
#
|
||||
# It is not expected to be run often (there is no go generate rule), and mainly
|
||||
# exists for documentation purposes.
|
||||
|
||||
cat > server.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
cat > client.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
EOF
|
||||
|
||||
# Create a certificate authority
|
||||
openssl genrsa -out caKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=webhook_authz_ca"
|
||||
|
||||
# Create a second certificate authority
|
||||
openssl genrsa -out badCAKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=webhook_authz_ca"
|
||||
|
||||
# Create a server certiticate
|
||||
openssl genrsa -out serverKey.pem 2048
|
||||
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_authz_server" -config server.conf
|
||||
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
|
||||
|
||||
# Create a client certiticate
|
||||
openssl genrsa -out clientKey.pem 2048
|
||||
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=webhook_authz_client" -config client.conf
|
||||
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
|
||||
|
||||
outfile=certs_test.go
|
||||
|
||||
cat > $outfile << EOF
|
||||
/*
|
||||
Copyright 2016 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.
|
||||
*/
|
||||
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
EOF
|
||||
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
||||
echo "var $file = []byte(\`$data\`)" >> $outfile
|
||||
done
|
||||
|
||||
# Clean up after we're done.
|
||||
rm ./*.pem
|
||||
rm ./*.csr
|
||||
rm ./*.srl
|
||||
rm ./*.conf
|
166
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics/metrics.go
generated
vendored
Normal file
166
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics/metrics.go
generated
vendored
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2021 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 metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/cel"
|
||||
compbasemetrics "k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
)
|
||||
|
||||
// AuthorizerMetrics specifies a set of methods that are used to register various metrics for the webhook authorizer
|
||||
type AuthorizerMetrics interface {
|
||||
// Request total and latency metrics
|
||||
RequestMetrics
|
||||
// Webhook count, latency, and fail open metrics
|
||||
WebhookMetrics
|
||||
// match condition metrics
|
||||
cel.MatcherMetrics
|
||||
}
|
||||
|
||||
type NoopAuthorizerMetrics struct {
|
||||
NoopRequestMetrics
|
||||
NoopWebhookMetrics
|
||||
cel.NoopMatcherMetrics
|
||||
}
|
||||
|
||||
type RequestMetrics interface {
|
||||
// RecordRequestTotal increments the total number of requests for the webhook authorizer
|
||||
RecordRequestTotal(ctx context.Context, code string)
|
||||
|
||||
// RecordRequestLatency measures request latency in seconds for webhooks. Broken down by status code.
|
||||
RecordRequestLatency(ctx context.Context, code string, latency float64)
|
||||
}
|
||||
|
||||
type NoopRequestMetrics struct{}
|
||||
|
||||
func (NoopRequestMetrics) RecordRequestTotal(context.Context, string) {}
|
||||
func (NoopRequestMetrics) RecordRequestLatency(context.Context, string, float64) {}
|
||||
|
||||
type WebhookMetrics interface {
|
||||
// RecordWebhookEvaluation increments with each round-trip of a webhook authorizer.
|
||||
// result is one of:
|
||||
// - canceled: the call invoking the webhook request was canceled
|
||||
// - timeout: the webhook request timed out
|
||||
// - error: the webhook response completed and was invalid
|
||||
// - success: the webhook response completed and was well-formed
|
||||
RecordWebhookEvaluation(ctx context.Context, name, result string)
|
||||
// RecordWebhookDuration records latency for each round-trip of a webhook authorizer.
|
||||
// result is one of:
|
||||
// - canceled: the call invoking the webhook request was canceled
|
||||
// - timeout: the webhook request timed out
|
||||
// - error: the webhook response completed and was invalid
|
||||
// - success: the webhook response completed and was well-formed
|
||||
RecordWebhookDuration(ctx context.Context, name, result string, duration float64)
|
||||
// RecordWebhookFailOpen increments when a webhook timeout or error results in a fail open
|
||||
// of a request which has not been canceled.
|
||||
// result is one of:
|
||||
// - timeout: the webhook request timed out
|
||||
// - error: the webhook response completed and was invalid
|
||||
RecordWebhookFailOpen(ctx context.Context, name, result string)
|
||||
}
|
||||
|
||||
type NoopWebhookMetrics struct{}
|
||||
|
||||
func (NoopWebhookMetrics) RecordWebhookEvaluation(ctx context.Context, name, result string) {}
|
||||
func (NoopWebhookMetrics) RecordWebhookDuration(ctx context.Context, name, result string, duration float64) {
|
||||
}
|
||||
func (NoopWebhookMetrics) RecordWebhookFailOpen(ctx context.Context, name, result string) {}
|
||||
|
||||
var registerWebhookMetrics sync.Once
|
||||
|
||||
// RegisterMetrics registers authorizer metrics.
|
||||
func RegisterWebhookMetrics() {
|
||||
registerWebhookMetrics.Do(func() {
|
||||
legacyregistry.MustRegister(webhookEvaluations)
|
||||
legacyregistry.MustRegister(webhookDuration)
|
||||
legacyregistry.MustRegister(webhookFailOpen)
|
||||
})
|
||||
}
|
||||
|
||||
func ResetMetricsForTest() {
|
||||
webhookEvaluations.Reset()
|
||||
webhookDuration.Reset()
|
||||
webhookFailOpen.Reset()
|
||||
}
|
||||
|
||||
const (
|
||||
namespace = "apiserver"
|
||||
subsystem = "authorization"
|
||||
)
|
||||
|
||||
var (
|
||||
webhookEvaluations = compbasemetrics.NewCounterVec(
|
||||
&compbasemetrics.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "webhook_evaluations_total",
|
||||
Help: "Round-trips to authorization webhooks.",
|
||||
StabilityLevel: compbasemetrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "result"},
|
||||
)
|
||||
|
||||
webhookDuration = compbasemetrics.NewHistogramVec(
|
||||
&compbasemetrics.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "webhook_duration_seconds",
|
||||
Help: "Request latency in seconds.",
|
||||
Buckets: compbasemetrics.DefBuckets,
|
||||
StabilityLevel: compbasemetrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "result"},
|
||||
)
|
||||
|
||||
webhookFailOpen = compbasemetrics.NewCounterVec(
|
||||
&compbasemetrics.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "webhook_evaluations_fail_open_total",
|
||||
Help: "NoOpinion results due to webhook timeout or error.",
|
||||
StabilityLevel: compbasemetrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "result"},
|
||||
)
|
||||
)
|
||||
|
||||
type webhookMetrics struct{}
|
||||
|
||||
func NewWebhookMetrics() WebhookMetrics {
|
||||
RegisterWebhookMetrics()
|
||||
return webhookMetrics{}
|
||||
}
|
||||
|
||||
func ResetWebhookMetricsForTest() {
|
||||
webhookEvaluations.Reset()
|
||||
webhookDuration.Reset()
|
||||
webhookFailOpen.Reset()
|
||||
}
|
||||
|
||||
func (webhookMetrics) RecordWebhookEvaluation(ctx context.Context, name, result string) {
|
||||
webhookEvaluations.WithContext(ctx).WithLabelValues(name, result).Inc()
|
||||
}
|
||||
func (webhookMetrics) RecordWebhookDuration(ctx context.Context, name, result string, duration float64) {
|
||||
webhookDuration.WithContext(ctx).WithLabelValues(name, result).Observe(duration)
|
||||
}
|
||||
func (webhookMetrics) RecordWebhookFailOpen(ctx context.Context, name, result string) {
|
||||
webhookFailOpen.WithContext(ctx).WithLabelValues(name, result).Inc()
|
||||
}
|
605
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go
generated
vendored
Normal file
605
e2e/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go
generated
vendored
Normal file
@ -0,0 +1,605 @@
|
||||
/*
|
||||
Copyright 2016 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 webhook implements the authorizer.Authorizer interface using HTTP webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// The maximum length of requester-controlled attributes to allow caching.
|
||||
maxControlledAttrCacheSize = 10000
|
||||
)
|
||||
|
||||
// DefaultRetryBackoff returns the default backoff parameters for webhook retry.
|
||||
func DefaultRetryBackoff() *wait.Backoff {
|
||||
backoff := webhook.DefaultRetryBackoffWithInitialDelay(500 * time.Millisecond)
|
||||
return &backoff
|
||||
}
|
||||
|
||||
// Ensure Webhook implements the authorizer.Authorizer interface.
|
||||
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
|
||||
|
||||
type subjectAccessReviewer interface {
|
||||
Create(context.Context, *authorizationv1.SubjectAccessReview, metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error)
|
||||
}
|
||||
|
||||
type WebhookAuthorizer struct {
|
||||
subjectAccessReview subjectAccessReviewer
|
||||
responseCache *cache.LRUExpireCache
|
||||
authorizedTTL time.Duration
|
||||
unauthorizedTTL time.Duration
|
||||
retryBackoff wait.Backoff
|
||||
decisionOnError authorizer.Decision
|
||||
metrics metrics.AuthorizerMetrics
|
||||
celMatcher *authorizationcel.CELMatcher
|
||||
name string
|
||||
}
|
||||
|
||||
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
|
||||
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, metrics metrics.AuthorizerMetrics, compiler authorizationcel.Compiler) (*WebhookAuthorizer, error) {
|
||||
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics, compiler, "")
|
||||
}
|
||||
|
||||
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
|
||||
// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
|
||||
//
|
||||
// # clusters refers to the remote service.
|
||||
// clusters:
|
||||
// - name: name-of-remote-authz-service
|
||||
// cluster:
|
||||
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
|
||||
// server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.
|
||||
//
|
||||
// # users refers to the API server's webhook configuration.
|
||||
// users:
|
||||
// - name: name-of-api-server
|
||||
// user:
|
||||
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
|
||||
// client-key: /path/to/key.pem # key matching the cert
|
||||
//
|
||||
// For additional HTTP configuration, refer to the kubeconfig documentation
|
||||
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
|
||||
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, name string, metrics metrics.AuthorizerMetrics, compiler authorizationcel.Compiler) (*WebhookAuthorizer, error) {
|
||||
subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, metrics, compiler, name)
|
||||
}
|
||||
|
||||
// newWithBackoff allows tests to skip the sleep.
|
||||
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, am metrics.AuthorizerMetrics, compiler authorizationcel.Compiler, name string) (*WebhookAuthorizer, error) {
|
||||
// compile all expressions once in validation and save the results to be used for eval later
|
||||
cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(compiler, matchConditions)
|
||||
if err := fieldErr.ToAggregate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cm != nil {
|
||||
cm.AuthorizerType = "Webhook"
|
||||
cm.AuthorizerName = name
|
||||
cm.Metrics = am
|
||||
}
|
||||
return &WebhookAuthorizer{
|
||||
subjectAccessReview: subjectAccessReview,
|
||||
responseCache: cache.NewLRUExpireCache(8192),
|
||||
authorizedTTL: authorizedTTL,
|
||||
unauthorizedTTL: unauthorizedTTL,
|
||||
retryBackoff: retryBackoff,
|
||||
decisionOnError: decisionOnError,
|
||||
metrics: am,
|
||||
celMatcher: cm,
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Authorize makes a REST request to the remote service describing the attempted action as a JSON
|
||||
// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
|
||||
// provided below.
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "spec": {
|
||||
// "resourceAttributes": {
|
||||
// "namespace": "kittensandponies",
|
||||
// "verb": "GET",
|
||||
// "group": "group3",
|
||||
// "resource": "pods"
|
||||
// },
|
||||
// "user": "jane",
|
||||
// "group": [
|
||||
// "group1",
|
||||
// "group2"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
|
||||
// disallow access. A permissive response would return:
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "status": {
|
||||
// "allowed": true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// To disallow access, the remote service would return:
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "status": {
|
||||
// "allowed": false,
|
||||
// "reason": "user does not have read access to the namespace"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// TODO(mikedanese): We should eventually support failing closed when we
|
||||
// encounter an error. We are failing open now to preserve backwards compatible
|
||||
// behavior.
|
||||
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||
r := &authorizationv1.SubjectAccessReview{}
|
||||
if user := attr.GetUser(); user != nil {
|
||||
r.Spec = authorizationv1.SubjectAccessReviewSpec{
|
||||
User: user.GetName(),
|
||||
UID: user.GetUID(),
|
||||
Groups: user.GetGroups(),
|
||||
Extra: convertToSARExtra(user.GetExtra()),
|
||||
}
|
||||
}
|
||||
|
||||
if attr.IsResourceRequest() {
|
||||
r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
|
||||
} else {
|
||||
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
|
||||
Path: attr.GetPath(),
|
||||
Verb: attr.GetVerb(),
|
||||
}
|
||||
}
|
||||
// skipping match when feature is not enabled
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
|
||||
// Process Match Conditions before calling the webhook
|
||||
matches, err := w.match(ctx, r)
|
||||
// If at least one matchCondition evaluates to an error (but none are FALSE):
|
||||
// If failurePolicy=Deny, then the webhook rejects the request
|
||||
// If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped
|
||||
if err != nil {
|
||||
return w.decisionOnError, "", err
|
||||
}
|
||||
// If at least one matchCondition successfully evaluates to FALSE,
|
||||
// then the webhook is skipped.
|
||||
if !matches {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
}
|
||||
// If all evaluated successfully and ALL matchConditions evaluate to TRUE,
|
||||
// then the webhook is called.
|
||||
key, err := json.Marshal(r.Spec)
|
||||
if err != nil {
|
||||
return w.decisionOnError, "", err
|
||||
}
|
||||
if entry, ok := w.responseCache.Get(string(key)); ok {
|
||||
r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
|
||||
} else {
|
||||
var result *authorizationv1.SubjectAccessReview
|
||||
var metricsResult string
|
||||
// WithExponentialBackoff will return SAR create error (sarErr) if any.
|
||||
if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
|
||||
var sarErr error
|
||||
var statusCode int
|
||||
|
||||
start := time.Now()
|
||||
result, statusCode, sarErr = w.subjectAccessReview.Create(ctx, r, metav1.CreateOptions{})
|
||||
latency := time.Since(start)
|
||||
|
||||
switch {
|
||||
case sarErr == nil:
|
||||
metricsResult = "success"
|
||||
case ctx.Err() != nil:
|
||||
metricsResult = "canceled"
|
||||
case utilnet.IsTimeout(sarErr) || errors.Is(sarErr, context.DeadlineExceeded) || apierrors.IsTimeout(sarErr) || statusCode == http.StatusGatewayTimeout:
|
||||
metricsResult = "timeout"
|
||||
default:
|
||||
metricsResult = "error"
|
||||
}
|
||||
w.metrics.RecordWebhookEvaluation(ctx, w.name, metricsResult)
|
||||
w.metrics.RecordWebhookDuration(ctx, w.name, metricsResult, latency.Seconds())
|
||||
|
||||
if statusCode != 0 {
|
||||
w.metrics.RecordRequestTotal(ctx, strconv.Itoa(statusCode))
|
||||
w.metrics.RecordRequestLatency(ctx, strconv.Itoa(statusCode), latency.Seconds())
|
||||
return sarErr
|
||||
}
|
||||
|
||||
if sarErr != nil {
|
||||
w.metrics.RecordRequestTotal(ctx, "<error>")
|
||||
w.metrics.RecordRequestLatency(ctx, "<error>", latency.Seconds())
|
||||
}
|
||||
|
||||
return sarErr
|
||||
}, webhook.DefaultShouldRetry); err != nil {
|
||||
klog.Errorf("Failed to make webhook authorizer request: %v", err)
|
||||
|
||||
// we're returning NoOpinion, and the parent context has not timed out or been canceled
|
||||
if w.decisionOnError == authorizer.DecisionNoOpinion && ctx.Err() == nil {
|
||||
w.metrics.RecordWebhookFailOpen(ctx, w.name, metricsResult)
|
||||
}
|
||||
|
||||
return w.decisionOnError, "", err
|
||||
}
|
||||
|
||||
r.Status = result.Status
|
||||
if shouldCache(attr) {
|
||||
if r.Status.Allowed {
|
||||
w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
|
||||
} else {
|
||||
w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case r.Status.Denied && r.Status.Allowed:
|
||||
return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
|
||||
case r.Status.Denied:
|
||||
return authorizer.DecisionDeny, r.Status.Reason, nil
|
||||
case r.Status.Allowed:
|
||||
return authorizer.DecisionAllow, r.Status.Reason, nil
|
||||
default:
|
||||
return authorizer.DecisionNoOpinion, r.Status.Reason, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func resourceAttributesFrom(attr authorizer.Attributes) *authorizationv1.ResourceAttributes {
|
||||
ret := &authorizationv1.ResourceAttributes{
|
||||
Namespace: attr.GetNamespace(),
|
||||
Verb: attr.GetVerb(),
|
||||
Group: attr.GetAPIGroup(),
|
||||
Version: attr.GetAPIVersion(),
|
||||
Resource: attr.GetResource(),
|
||||
Subresource: attr.GetSubresource(),
|
||||
Name: attr.GetName(),
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
|
||||
// If we are able to get any requirements while parsing selectors, use them, even if there's an error.
|
||||
// This is because selectors only narrow, so if a subset of selector requirements are available, the request can be allowed.
|
||||
if selectorRequirements, _ := fieldSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
|
||||
ret.FieldSelector = &authorizationv1.FieldSelectorAttributes{
|
||||
Requirements: selectorRequirements,
|
||||
}
|
||||
}
|
||||
|
||||
if selectorRequirements, _ := labelSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
|
||||
ret.LabelSelector = &authorizationv1.LabelSelectorAttributes{
|
||||
Requirements: selectorRequirements,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func fieldSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.FieldSelectorRequirement, error) {
|
||||
requirements, getFieldSelectorErr := attr.GetFieldSelector()
|
||||
if len(requirements) == 0 {
|
||||
return nil, getFieldSelectorErr
|
||||
}
|
||||
|
||||
retRequirements := []metav1.FieldSelectorRequirement{}
|
||||
for _, requirement := range requirements {
|
||||
retRequirement := metav1.FieldSelectorRequirement{}
|
||||
switch {
|
||||
case requirement.Operator == selection.Equals || requirement.Operator == selection.DoubleEquals || requirement.Operator == selection.In:
|
||||
retRequirement.Operator = metav1.FieldSelectorOpIn
|
||||
retRequirement.Key = requirement.Field
|
||||
retRequirement.Values = []string{requirement.Value}
|
||||
case requirement.Operator == selection.NotEquals || requirement.Operator == selection.NotIn:
|
||||
retRequirement.Operator = metav1.FieldSelectorOpNotIn
|
||||
retRequirement.Key = requirement.Field
|
||||
retRequirement.Values = []string{requirement.Value}
|
||||
default:
|
||||
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
|
||||
// for authorization since the resulting check will only be as broad or broader than the intended.
|
||||
continue
|
||||
}
|
||||
retRequirements = append(retRequirements, retRequirement)
|
||||
}
|
||||
|
||||
if len(retRequirements) == 0 {
|
||||
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
|
||||
// unrestricted action.
|
||||
return nil, getFieldSelectorErr
|
||||
}
|
||||
return retRequirements, getFieldSelectorErr
|
||||
}
|
||||
|
||||
func labelSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.LabelSelectorRequirement, error) {
|
||||
requirements, getLabelSelectorErr := attr.GetLabelSelector()
|
||||
if len(requirements) == 0 {
|
||||
return nil, getLabelSelectorErr
|
||||
}
|
||||
|
||||
retRequirements := []metav1.LabelSelectorRequirement{}
|
||||
for _, requirement := range requirements {
|
||||
retRequirement := metav1.LabelSelectorRequirement{
|
||||
Key: requirement.Key(),
|
||||
}
|
||||
if values := requirement.ValuesUnsorted(); len(values) > 0 {
|
||||
retRequirement.Values = values
|
||||
}
|
||||
switch requirement.Operator() {
|
||||
case selection.Equals, selection.DoubleEquals, selection.In:
|
||||
retRequirement.Operator = metav1.LabelSelectorOpIn
|
||||
case selection.NotEquals, selection.NotIn:
|
||||
retRequirement.Operator = metav1.LabelSelectorOpNotIn
|
||||
case selection.Exists:
|
||||
retRequirement.Operator = metav1.LabelSelectorOpExists
|
||||
case selection.DoesNotExist:
|
||||
retRequirement.Operator = metav1.LabelSelectorOpDoesNotExist
|
||||
default:
|
||||
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
|
||||
// for authorization since the resulting check will only be as broad or broader than the intended.
|
||||
continue
|
||||
}
|
||||
retRequirements = append(retRequirements, retRequirement)
|
||||
}
|
||||
|
||||
if len(retRequirements) == 0 {
|
||||
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
|
||||
// unrestricted action.
|
||||
return nil, getLabelSelectorErr
|
||||
}
|
||||
return retRequirements, getLabelSelectorErr
|
||||
}
|
||||
|
||||
// TODO: need to finish the method to get the rules when using webhook mode
|
||||
func (w *WebhookAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
|
||||
var (
|
||||
resourceRules []authorizer.ResourceRuleInfo
|
||||
nonResourceRules []authorizer.NonResourceRuleInfo
|
||||
)
|
||||
incomplete := true
|
||||
return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
|
||||
}
|
||||
|
||||
// Match is used to evaluate the SubjectAccessReviewSpec against
|
||||
// the authorizer's matchConditions in the form of cel expressions
|
||||
// to return match or no match found, which then is used to
|
||||
// determine if the webhook should be skipped.
|
||||
func (w *WebhookAuthorizer) match(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
|
||||
// A nil celMatcher or zero saved CompilationResults matches all requests.
|
||||
if w.celMatcher == nil || w.celMatcher.CompilationResults == nil {
|
||||
return true, nil
|
||||
}
|
||||
return w.celMatcher.Eval(ctx, r)
|
||||
}
|
||||
|
||||
func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.ExtraValue {
|
||||
if extra == nil {
|
||||
return nil
|
||||
}
|
||||
ret := map[string]authorizationv1.ExtraValue{}
|
||||
for k, v := range extra {
|
||||
ret[k] = authorizationv1.ExtraValue(v)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// subjectAccessReviewInterfaceFromConfig builds a client from the specified kubeconfig file,
|
||||
// and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
|
||||
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
||||
func subjectAccessReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (subjectAccessReviewer, error) {
|
||||
localScheme := runtime.NewScheme()
|
||||
if err := scheme.AddToScheme(localScheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch version {
|
||||
case authorizationv1.SchemeGroupVersion.Version:
|
||||
groupVersions := []schema.GroupVersion{authorizationv1.SchemeGroupVersion}
|
||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &subjectAccessReviewV1ClientGW{gw.RestClient}, nil
|
||||
|
||||
case authorizationv1beta1.SchemeGroupVersion.Version:
|
||||
groupVersions := []schema.GroupVersion{authorizationv1beta1.SchemeGroupVersion}
|
||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &subjectAccessReviewV1beta1ClientGW{gw.RestClient}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported webhook authorizer version %q, supported versions are %q, %q",
|
||||
version,
|
||||
authorizationv1.SchemeGroupVersion.Version,
|
||||
authorizationv1beta1.SchemeGroupVersion.Version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type subjectAccessReviewV1Client struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
func (t *subjectAccessReviewV1Client) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, opts metav1.CreateOptions) (result *authorizationv1.SubjectAccessReview, statusCode int, err error) {
|
||||
result = &authorizationv1.SubjectAccessReview{}
|
||||
|
||||
restResult := t.client.Post().
|
||||
Resource("subjectaccessreviews").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Body(subjectAccessReview).
|
||||
Do(ctx)
|
||||
|
||||
restResult.StatusCode(&statusCode)
|
||||
err = restResult.Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// subjectAccessReviewV1ClientGW used by the generic webhook, doesn't specify GVR.
|
||||
type subjectAccessReviewV1ClientGW struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
func (t *subjectAccessReviewV1ClientGW) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, _ metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error) {
|
||||
var statusCode int
|
||||
result := &authorizationv1.SubjectAccessReview{}
|
||||
|
||||
restResult := t.client.Post().Body(subjectAccessReview).Do(ctx)
|
||||
|
||||
restResult.StatusCode(&statusCode)
|
||||
err := restResult.Into(result)
|
||||
|
||||
return result, statusCode, err
|
||||
}
|
||||
|
||||
// subjectAccessReviewV1beta1ClientGW used by the generic webhook, doesn't specify GVR.
|
||||
type subjectAccessReviewV1beta1ClientGW struct {
|
||||
client rest.Interface
|
||||
}
|
||||
|
||||
func (t *subjectAccessReviewV1beta1ClientGW) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, _ metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error) {
|
||||
var statusCode int
|
||||
v1beta1Review := &authorizationv1beta1.SubjectAccessReview{Spec: v1SpecToV1beta1Spec(&subjectAccessReview.Spec)}
|
||||
v1beta1Result := &authorizationv1beta1.SubjectAccessReview{}
|
||||
|
||||
restResult := t.client.Post().Body(v1beta1Review).Do(ctx)
|
||||
|
||||
restResult.StatusCode(&statusCode)
|
||||
err := restResult.Into(v1beta1Result)
|
||||
if err == nil {
|
||||
subjectAccessReview.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
|
||||
}
|
||||
return subjectAccessReview, statusCode, err
|
||||
}
|
||||
|
||||
// shouldCache determines whether it is safe to cache the given request attributes. If the
|
||||
// requester-controlled attributes are too large, this may be a DoS attempt, so we skip the cache.
|
||||
func shouldCache(attr authorizer.Attributes) bool {
|
||||
controlledAttrSize := int64(len(attr.GetNamespace())) +
|
||||
int64(len(attr.GetVerb())) +
|
||||
int64(len(attr.GetAPIGroup())) +
|
||||
int64(len(attr.GetAPIVersion())) +
|
||||
int64(len(attr.GetResource())) +
|
||||
int64(len(attr.GetSubresource())) +
|
||||
int64(len(attr.GetName())) +
|
||||
int64(len(attr.GetPath()))
|
||||
return controlledAttrSize < maxControlledAttrCacheSize
|
||||
}
|
||||
|
||||
func v1beta1StatusToV1Status(in *authorizationv1beta1.SubjectAccessReviewStatus) authorizationv1.SubjectAccessReviewStatus {
|
||||
return authorizationv1.SubjectAccessReviewStatus{
|
||||
Allowed: in.Allowed,
|
||||
Denied: in.Denied,
|
||||
Reason: in.Reason,
|
||||
EvaluationError: in.EvaluationError,
|
||||
}
|
||||
}
|
||||
|
||||
func v1SpecToV1beta1Spec(in *authorizationv1.SubjectAccessReviewSpec) authorizationv1beta1.SubjectAccessReviewSpec {
|
||||
return authorizationv1beta1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: v1ResourceAttributesToV1beta1ResourceAttributes(in.ResourceAttributes),
|
||||
NonResourceAttributes: v1NonResourceAttributesToV1beta1NonResourceAttributes(in.NonResourceAttributes),
|
||||
User: in.User,
|
||||
Groups: in.Groups,
|
||||
Extra: v1ExtraToV1beta1Extra(in.Extra),
|
||||
UID: in.UID,
|
||||
}
|
||||
}
|
||||
|
||||
func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.ResourceAttributes) *authorizationv1beta1.ResourceAttributes {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
return &authorizationv1beta1.ResourceAttributes{
|
||||
Namespace: in.Namespace,
|
||||
Verb: in.Verb,
|
||||
Group: in.Group,
|
||||
Version: in.Version,
|
||||
Resource: in.Resource,
|
||||
Subresource: in.Subresource,
|
||||
Name: in.Name,
|
||||
FieldSelector: in.FieldSelector,
|
||||
LabelSelector: in.LabelSelector,
|
||||
}
|
||||
}
|
||||
|
||||
func v1NonResourceAttributesToV1beta1NonResourceAttributes(in *authorizationv1.NonResourceAttributes) *authorizationv1beta1.NonResourceAttributes {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
return &authorizationv1beta1.NonResourceAttributes{
|
||||
Path: in.Path,
|
||||
Verb: in.Verb,
|
||||
}
|
||||
}
|
||||
|
||||
func v1ExtraToV1beta1Extra(in map[string]authorizationv1.ExtraValue) map[string]authorizationv1beta1.ExtraValue {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(map[string]authorizationv1beta1.ExtraValue, len(in))
|
||||
for k, v := range in {
|
||||
ret[k] = authorizationv1beta1.ExtraValue(v)
|
||||
}
|
||||
return ret
|
||||
}
|
Reference in New Issue
Block a user