diff --git a/helper/backend/backend.go b/helper/backend/backend.go new file mode 100644 index 0000000000..b540245142 --- /dev/null +++ b/helper/backend/backend.go @@ -0,0 +1,61 @@ +package backend + +import ( + "github.com/hashicorp/vault/vault" +) + +// Backend is an implementation of vault.LogicalBackend that allows +// the implementer to code a backend using a much more programmer-friendly +// framework that handles a lot of the routing and validation for you. +// +// This is recommended over implementing vault.LogicalBackend directly. +type Backend struct { + Paths []*Path +} + +// Path is a single path that the backend responds to. +type Path struct { + // Pattern is the pattern of the URL that matches this path. + // + // This should be a valid regular expression. Named captures will be + // exposed as fields that should map to a schema in Fields. If a named + // capture is not a field in the Fields map, then it will be ignored. + Pattern string + + // Fields is the mapping of data fields to a schema describing that + // field. Named captures in the Pattern also map to fields. If a named + // capture name matches a PUT body name, the named capture takes + // priority. + // + // Note that only named capture fields are available in every operation, + // whereas all fields are avaiable in the Write operation. + Fields map[string]*FieldSchema + + // Root if not blank, denotes that this path requires root + // privileges and the path pattern that is the root path. This can't + // be a regular expression and must be an exact path. It may have a + // trailing '*' to denote that it is a prefix, and not an exact match. + Root string + + // Callback is what is called when this path is requested with + // a valid set of data. + Callback func(*vault.Request, *FieldData) (*vault.Response, error) +} + +// FieldSchema is a basic schema to describe the format of a path field. +type FieldSchema struct { + Type FieldType +} + +func (t FieldType) Zero() interface{} { + switch t { + case TypeString: + return "" + case TypeInt: + return 0 + case TypeBool: + return false + default: + panic("unknown type: " + t.String()) + } +} diff --git a/helper/backend/field_data.go b/helper/backend/field_data.go new file mode 100644 index 0000000000..d9117d5d85 --- /dev/null +++ b/helper/backend/field_data.go @@ -0,0 +1,111 @@ +package backend + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +// FieldData is the structure passed to the callback to handle a path +// containing the populated parameters for fields. This should be used +// instead of the raw (*vault.Request).Data to access data in a type-safe +// way. +type FieldData struct { + Raw map[string]interface{} + Schema map[string]*FieldSchema +} + +// Get gets the value for the given field. If the key is an invalid field, +// FieldData will panic. If you want a safer version of this method, use +// GetOk. If the field k is not set, the default value (if set) will be +// returned, otherwise the zero value will be returned. +func (d *FieldData) Get(k string) interface{} { + schema, ok := d.Schema[k] + if !ok { + panic(fmt.Sprintf("field %s not in the schema", k)) + } + + value, ok := d.GetOk(k) + if !ok { + value = schema.Type.Zero() + } + + return value +} + +// GetOk gets the value for the given field. The second return value +// will be false if the key is invalid or the key is not set at all. +func (d *FieldData) GetOk(k string) (interface{}, bool) { + schema, ok := d.Schema[k] + if !ok { + return nil, false + } + + result, ok, err := d.GetOkErr(k) + if err != nil { + panic(fmt.Sprintf("error reading %s: %s", k, err)) + } + + if ok && result == nil { + result = schema.Type.Zero() + } + + return result, ok +} + +// GetOkErr is the most conservative of all the Get methods. It returns +// whether key is set or not, but also an error value. The error value is +// non-nil if the field doesn't exist or there was an error parsing the +// field value. +func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) { + schema, ok := d.Schema[k] + if !ok { + return nil, false, fmt.Errorf("unknown field: %s", k) + } + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return d.getPrimitive(k, schema) + default: + return nil, false, + fmt.Errorf("unknown field type %s for field %s", schema.Type, k) + } +} + +func (d *FieldData) getPrimitive( + k string, schema *FieldSchema) (interface{}, bool, error) { + raw, ok := d.Raw[k] + if !ok { + return nil, false, nil + } + + switch schema.Type { + case TypeBool: + var result bool + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, true, err + } + + return result, true, nil + case TypeInt: + var result int + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, true, err + } + + return result, true, nil + case TypeString: + var result string + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, true, err + } + + return result, true, nil + default: + panic(fmt.Sprintf("Unknown type: %s", schema.Type)) + } +} diff --git a/helper/backend/field_data_test.go b/helper/backend/field_data_test.go new file mode 100644 index 0000000000..116df2d95f --- /dev/null +++ b/helper/backend/field_data_test.go @@ -0,0 +1,82 @@ +package backend + +import ( + "reflect" + "testing" +) + +func TestFieldDataGet(t *testing.T) { + cases := map[string]struct { + Schema map[string]*FieldSchema + Raw map[string]interface{} + Key string + Value interface{} + }{ + "string type, string value": { + map[string]*FieldSchema{ + "foo": &FieldSchema{Type: TypeString}, + }, + map[string]interface{}{ + "foo": "bar", + }, + "foo", + "bar", + }, + + "string type, int value": { + map[string]*FieldSchema{ + "foo": &FieldSchema{Type: TypeString}, + }, + map[string]interface{}{ + "foo": 42, + }, + "foo", + "42", + }, + + "string type, unset value": { + map[string]*FieldSchema{ + "foo": &FieldSchema{Type: TypeString}, + }, + map[string]interface{}{}, + "foo", + "", + }, + + "int type, int value": { + map[string]*FieldSchema{ + "foo": &FieldSchema{Type: TypeInt}, + }, + map[string]interface{}{ + "foo": 42, + }, + "foo", + 42, + }, + + "bool type, bool value": { + map[string]*FieldSchema{ + "foo": &FieldSchema{Type: TypeBool}, + }, + map[string]interface{}{ + "foo": false, + }, + "foo", + false, + }, + } + + for name, tc := range cases { + data := &FieldData{ + Raw: tc.Raw, + Schema: tc.Schema, + } + + actual := data.Get(tc.Key) + if !reflect.DeepEqual(actual, tc.Value) { + t.Fatalf( + "bad: %s\n\nExpected: %#v\nGot: %#v", + name, tc.Value, actual) + } + } +} diff --git a/helper/backend/field_type.go b/helper/backend/field_type.go new file mode 100644 index 0000000000..80289170fb --- /dev/null +++ b/helper/backend/field_type.go @@ -0,0 +1,16 @@ +package backend + +//go:generate stringer -type=FieldType field_type.go + +// FieldType is the enum of types that a field can be. +type FieldType uint + +const ( + TypeInvalid FieldType = 0 + TypeString FieldType = iota + TypeInt + TypeBool +) + +// FieldType has more methods defined on it in backend.go. They aren't +// in this file since stringer doesn't like that. diff --git a/helper/backend/fieldtype_string.go b/helper/backend/fieldtype_string.go new file mode 100644 index 0000000000..3f537a9377 --- /dev/null +++ b/helper/backend/fieldtype_string.go @@ -0,0 +1,16 @@ +// generated by stringer -type=FieldType field_type.go; DO NOT EDIT + +package backend + +import "fmt" + +const _FieldType_name = "TypeInvalidTypeStringTypeIntTypeBool" + +var _FieldType_index = [...]uint8{0, 11, 21, 28, 36} + +func (i FieldType) String() string { + if i+1 >= FieldType(len(_FieldType_index)) { + return fmt.Sprintf("FieldType(%d)", i) + } + return _FieldType_name[_FieldType_index[i]:_FieldType_index[i+1]] +}