Files
Marek Smoliński bbc82e62ff Add DeleteBatch scaffolding and tests (#66258)
* Add DeleteBatch scaffolding and tests

* update

* Deduplicate keys

* Add type assertion
2026-05-04 11:39:25 +00:00

240 lines
7.1 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package backend
import (
"context"
"iter"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
)
// errorMessage is the error message to return when invalid input is provided by the caller.
const errorMessage = "special characters are not allowed in resource names, please use name composed only from characters, hyphens, dots, and plus signs: %q"
// isValidKeyByte checks if the byte is a valid character for a key.
func isValidKeyByte(b byte) bool {
switch b {
case
// Digits
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// Lowercase letters
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
// Uppercase letters
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
// Allowed symbols
'@', '_', ':', '.', '-', '+':
return true
default:
return false
}
}
// IsKeySafe checks if the passed in key conforms to whitelist
func IsKeySafe(key Key) bool {
for i, k := range key.components {
switch k {
case noEnd:
continue
case ".", "..":
return false
case "":
return key.exactKey && i == len(key.components)-1
}
for _, b := range []byte(k) {
if !isValidKeyByte(b) {
return false
}
}
}
return true
}
var _ Backend = (*Sanitizer)(nil)
var _ BatchDeleter = (*Sanitizer)(nil)
var _ BatchPutter = (*Sanitizer)(nil)
// Sanitizer wraps a [Backend] implementation to make sure all
// [Key]s written to the backend are allowed. Retrieval and deletion
// of items do not perform any [Key] sanitization in order to allow
// interacting with any items that might already exist in the
// [Backend] prior to validation being performed on each
// subcomponent of a [Key] instead of on the entire [Key].
type Sanitizer struct {
backend Backend
}
// NewSanitizer returns a new Sanitizer.
func NewSanitizer(backend Backend) *Sanitizer {
return &Sanitizer{
backend: backend,
}
}
// GetRange returns query range
func (s *Sanitizer) GetRange(ctx context.Context, startKey, endKey Key, limit int) (*GetResult, error) {
return s.backend.GetRange(ctx, startKey, endKey, limit)
}
func (s *Sanitizer) Items(ctx context.Context, params ItemsParams) iter.Seq2[Item, error] {
return s.backend.Items(ctx, params)
}
// Create creates item if it does not exist
func (s *Sanitizer) Create(ctx context.Context, i Item) (*Lease, error) {
if !IsKeySafe(i.Key) {
return nil, trace.BadParameter(errorMessage, i.Key)
}
return s.backend.Create(ctx, i)
}
// Put puts value into backend (creates if it does not
// exists, updates it otherwise)
func (s *Sanitizer) Put(ctx context.Context, i Item) (*Lease, error) {
if !IsKeySafe(i.Key) {
return nil, trace.BadParameter(errorMessage, i.Key)
}
return s.backend.Put(ctx, i)
}
// PutBatch puts multiple values into backend.
func (s *Sanitizer) PutBatch(ctx context.Context, items []Item) ([]string, error) {
for _, item := range items {
if !IsKeySafe(item.Key) {
return nil, trace.BadParameter(errorMessage, item.Key)
}
}
out, err := PutBatch(ctx, s.backend, items)
return out, trace.Wrap(err)
}
// Update updates value in the backend
func (s *Sanitizer) Update(ctx context.Context, i Item) (*Lease, error) {
if !IsKeySafe(i.Key) {
return nil, trace.BadParameter(errorMessage, i.Key)
}
return s.backend.Update(ctx, i)
}
// ConditionalUpdate updates the value in the backend if the revision of the [Item] matches
// the stored revision.
func (s *Sanitizer) ConditionalUpdate(ctx context.Context, i Item) (*Lease, error) {
if !IsKeySafe(i.Key) {
return nil, trace.BadParameter(errorMessage, i.Key)
}
return s.backend.ConditionalUpdate(ctx, i)
}
// Get returns a single item or not found error
func (s *Sanitizer) Get(ctx context.Context, key Key) (*Item, error) {
return s.backend.Get(ctx, key)
}
// CompareAndSwap compares item with existing item
// and replaces is with replaceWith item
func (s *Sanitizer) CompareAndSwap(ctx context.Context, expected Item, replaceWith Item) (*Lease, error) {
if !IsKeySafe(expected.Key) {
return nil, trace.BadParameter(errorMessage, expected.Key)
}
return s.backend.CompareAndSwap(ctx, expected, replaceWith)
}
// Delete deletes item by key
func (s *Sanitizer) Delete(ctx context.Context, key Key) error {
return s.backend.Delete(ctx, key)
}
// DeleteBatch deletes multiple items from the backend.
func (s *Sanitizer) DeleteBatch(ctx context.Context, keys []Key) error {
return trace.Wrap(DeleteBatch(ctx, s.backend, keys))
}
// ConditionalDelete deletes the item by key if the revision matches the stored revision.
func (s *Sanitizer) ConditionalDelete(ctx context.Context, key Key, revision string) error {
return s.backend.ConditionalDelete(ctx, key, revision)
}
// DeleteRange deletes range of items
func (s *Sanitizer) DeleteRange(ctx context.Context, startKey, endKey Key) error {
return s.backend.DeleteRange(ctx, startKey, endKey)
}
func (s *Sanitizer) AtomicWrite(ctx context.Context, condacts []ConditionalAction) (revision string, err error) {
for _, ca := range condacts {
if !IsKeySafe(ca.Key) {
return "", trace.BadParameter(errorMessage, ca.Key)
}
}
return s.backend.AtomicWrite(ctx, condacts)
}
// KeepAlive keeps object from expiring, updates lease on the existing object,
// expires contains the new expiry to set on the lease,
// some backends may ignore expires based on the implementation
// in case if the lease managed server side
func (s *Sanitizer) KeepAlive(ctx context.Context, lease Lease, expires time.Time) error {
if !IsKeySafe(lease.Key) {
return trace.BadParameter(errorMessage, lease.Key)
}
return s.backend.KeepAlive(ctx, lease, expires)
}
// NewWatcher returns a new event watcher
func (s *Sanitizer) NewWatcher(ctx context.Context, watch Watch) (Watcher, error) {
for _, prefix := range watch.Prefixes {
if !IsKeySafe(prefix) {
return nil, trace.BadParameter(errorMessage, prefix)
}
}
return s.backend.NewWatcher(ctx, watch)
}
// Close releases the resources taken up by this backend
func (s *Sanitizer) Close() error {
return s.backend.Close()
}
// Clock returns clock used by this backend
func (s *Sanitizer) Clock() clockwork.Clock {
return s.backend.Clock()
}
// CloseWatchers closes all the watchers
// without closing the backend
func (s *Sanitizer) CloseWatchers() {
s.backend.CloseWatchers()
}
func (s *Sanitizer) GetName() string {
return s.backend.GetName()
}