mirror of
https://github.com/gravitational/teleport
synced 2024-10-22 10:13:21 +00:00
257274b26f
- Also addresses #3282 by adding retries for CompareAndSwap on SetAccessRequestState and UpdatePluginData.
314 lines
8.2 KiB
Go
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))
|
|
}
|