teleport/lib/services/plugin_data.go
Forrest Marshall 257274b26f Implement per-resource PluginData storage (#3286)
- Also addresses #3282 by adding retries for CompareAndSwap
on SetAccessRequestState and UpdatePluginData.
2020-01-30 14:27:40 -08:00

314 lines
8.2 KiB
Go

/*
Copyright 2020 Gravitational, Inc.
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 services
import (
"fmt"
"time"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
)
// PluginData is used by plugins to store per-resource state. An instance of PluginData
// corresponds to a resource which may be managed by one or more plugins. Data is stored
// as a mapping of the form `plugin -> key -> val`, effectively giving each plugin its own
// key-value store. Importantly, an instance of PluginData can only be created for a resource
// which currently exist, and automatically expires shortly after the corresponding resource.
// Currently, only the AccessRequest resource is supported.
type PluginData interface {
Resource
// Entries gets all entries.
Entries() map[string]*PluginDataEntry
// Update attempts to apply an update.
Update(params PluginDataUpdateParams) error
// CheckAndSetDefaults validates the plugin data
// and supplies default values where appropriate.
CheckAndSetDefaults() error
}
func (d *PluginDataV3) CheckAndSetDefaults() error {
if err := d.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if d.SubKind == "" {
return trace.BadParameter("plugin data missing subkind")
}
return nil
}
// NewPluginData configures a new PluginData instance associated
// with the supplied resource name (currently, this must be the
// name of an access request).
func NewPluginData(resourceName string, resourceKind string) (PluginData, error) {
data := PluginDataV3{
Kind: KindPluginData,
Version: V3,
// If additional resource kinds become supported, make
// this a parameter.
SubKind: resourceKind,
Metadata: Metadata{
Name: resourceName,
},
Spec: PluginDataSpecV3{
Entries: make(map[string]*PluginDataEntry),
},
}
if err := data.CheckAndSetDefaults(); err != nil {
return nil, err
}
return &data, nil
}
func (d *PluginDataV3) Entries() map[string]*PluginDataEntry {
if d.Spec.Entries == nil {
d.Spec.Entries = make(map[string]*PluginDataEntry)
}
return d.Spec.Entries
}
func (d *PluginDataV3) Update(params PluginDataUpdateParams) error {
// See #3286 for a complete discussion of the design constraints at play here.
if params.Kind != d.GetSubKind() {
return trace.BadParameter("resource kind mismatch in update params")
}
if params.Resource != d.GetName() {
return trace.BadParameter("resource name mismatch in update params")
}
// If expectations were given, ensure that they are met before continuing
if params.Expect != nil {
if err := d.checkExpectations(params.Plugin, params.Expect); err != nil {
return trace.Wrap(err)
}
}
// Ensure that Entries has been initialized
if d.Spec.Entries == nil {
d.Spec.Entries = make(map[string]*PluginDataEntry, 1)
}
// Ensure that the specific Plugin has been initialized
if d.Spec.Entries[params.Plugin] == nil {
d.Spec.Entries[params.Plugin] = &PluginDataEntry{
Data: make(map[string]string, len(params.Set)),
}
}
entry := d.Spec.Entries[params.Plugin]
for key, val := range params.Set {
// Keys which are explicitly set to the empty string are
// treated as DELETE operations.
if val == "" {
delete(entry.Data, key)
continue
}
entry.Data[key] = val
}
// Its possible that this update was simply clearing all data;
// if that is the case, remove the entry.
if len(entry.Data) == 0 {
delete(d.Spec.Entries, params.Plugin)
}
return nil
}
// checkExpectations verifies that the data for `plugin` matches the expected
// state described by `expect`. This function implements the behavior of the
// `PluginDataUpdateParams.Expect` mapping.
func (d *PluginDataV3) checkExpectations(plugin string, expect map[string]string) error {
var entry *PluginDataEntry
if d.Spec.Entries != nil {
entry = d.Spec.Entries[plugin]
}
if entry == nil {
// If no entry currently exists, then the only expectation that can
// match is one which only specifies fields which shouldn't exist.
for key, val := range expect {
if val != "" {
return trace.CompareFailed("expectations not met for field %q", key)
}
}
return nil
}
for key, val := range expect {
if entry.Data[key] != val {
return trace.CompareFailed("expectations not met for field %q", key)
}
}
return nil
}
func (f *PluginDataFilter) Match(data PluginData) bool {
if f.Kind != "" && f.Kind != data.GetSubKind() {
return false
}
if f.Resource != "" && f.Resource != data.GetName() {
return false
}
if f.Plugin != "" {
if _, ok := data.Entries()[f.Plugin]; !ok {
return false
}
}
return true
}
func (d *PluginDataEntry) Equals(other *PluginDataEntry) bool {
if other == nil {
return false
}
if len(d.Data) != len(other.Data) {
return false
}
for key, val := range d.Data {
if other.Data[key] != val {
return false
}
}
return true
}
type PluginDataMarshaler interface {
MarshalPluginData(req PluginData, opts ...MarshalOption) ([]byte, error)
UnmarshalPluginData(bytes []byte, opts ...MarshalOption) (PluginData, error)
}
type pluginDataMarshaler struct{}
func (m *pluginDataMarshaler) MarshalPluginData(data PluginData, opts ...MarshalOption) ([]byte, error) {
cfg, err := collectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
switch r := data.(type) {
case *PluginDataV3:
if !cfg.PreserveResourceID {
// avoid modifying the original object
// to prevent unexpected data races
cp := *r
cp.SetResourceID(0)
r = &cp
}
return utils.FastMarshal(r)
default:
return nil, trace.BadParameter("unrecognized plugin data type: %T", data)
}
}
func (m *pluginDataMarshaler) UnmarshalPluginData(raw []byte, opts ...MarshalOption) (PluginData, error) {
cfg, err := collectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
var data PluginDataV3
if cfg.SkipValidation {
if err := utils.FastUnmarshal(raw, &data); err != nil {
return nil, trace.Wrap(err)
}
} else {
if err := utils.UnmarshalWithSchema(GetPluginDataSchema(), &data, raw); err != nil {
return nil, trace.Wrap(err)
}
}
if err := data.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
if cfg.ID != 0 {
data.SetResourceID(cfg.ID)
}
if !cfg.Expires.IsZero() {
data.SetExpiry(cfg.Expires)
}
return &data, nil
}
var pluginDataMarshalerInstance PluginDataMarshaler = &pluginDataMarshaler{}
func GetPluginDataMarshaler() PluginDataMarshaler {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
return pluginDataMarshalerInstance
}
const PluginDataSpecSchema = `{
"type": "object",
"additionalProperties": false,
"properties": {
"entries": { "type":"object" }
}
}`
func GetPluginDataSchema() string {
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, PluginDataSpecSchema, DefaultDefinitions)
}
func (r *PluginDataV3) GetKind() string {
return r.Kind
}
func (r *PluginDataV3) GetSubKind() string {
return r.SubKind
}
func (r *PluginDataV3) SetSubKind(subKind string) {
r.SubKind = subKind
}
func (r *PluginDataV3) GetVersion() string {
return r.Version
}
func (r *PluginDataV3) GetName() string {
return r.Metadata.Name
}
func (r *PluginDataV3) SetName(name string) {
r.Metadata.Name = name
}
func (r *PluginDataV3) Expiry() time.Time {
return r.Metadata.Expiry()
}
func (r *PluginDataV3) SetExpiry(expiry time.Time) {
r.Metadata.SetExpiry(expiry)
}
func (r *PluginDataV3) SetTTL(clock clockwork.Clock, ttl time.Duration) {
r.Metadata.SetTTL(clock, ttl)
}
func (r *PluginDataV3) GetMetadata() Metadata {
return r.Metadata
}
func (r *PluginDataV3) GetResourceID() int64 {
return r.Metadata.GetID()
}
func (r *PluginDataV3) SetResourceID(id int64) {
r.Metadata.SetID(id)
}
func (d *PluginDataV3) String() string {
return fmt.Sprintf("PluginData(kind=%s,resource=%s,entries=%d)", d.GetSubKind(), d.GetName(), len(d.Spec.Entries))
}