Merge branch 'master' of https://github.com/hashicorp/vault into vishalvault
This commit is contained in:
commit
f39df58eef
4
Godeps/Godeps.json
generated
4
Godeps/Godeps.json
generated
|
@ -63,6 +63,10 @@
|
|||
"Comment": "v2.0.0-7-g73a8ef7",
|
||||
"Rev": "73a8ef737e8ea002281a28b4cb92a1de121ad4c6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/fatih/structs",
|
||||
"Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-sql-driver/mysql",
|
||||
"Comment": "v1.2-88-ga197e5d",
|
||||
|
|
23
Godeps/_workspace/src/github.com/fatih/structs/.gitignore
generated
vendored
Normal file
23
Godeps/_workspace/src/github.com/fatih/structs/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
11
Godeps/_workspace/src/github.com/fatih/structs/.travis.yml
generated
vendored
Normal file
11
Godeps/_workspace/src/github.com/fatih/structs/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
language: go
|
||||
go: 1.3
|
||||
before_install:
|
||||
- go get github.com/axw/gocov/gocov
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -repotoken $COVERALLS_TOKEN
|
||||
env:
|
||||
global:
|
||||
- secure: hkc+92KPmMFqIH9n4yWdnH1JpZjahmOyDJwpTh8Yl0JieJNG0XEXpOqNao27eA0cLF+UHdyjFeGcPUJKNmgE46AoQjtovt+ICjCXKR2yF6S2kKJcUOz/Vd6boZF7qHV06jjxyxOebpID5iSoW6UfFr001bFxpd3jaSLFTzSHWRQ=
|
21
Godeps/_workspace/src/github.com/fatih/structs/LICENSE
generated
vendored
Normal file
21
Godeps/_workspace/src/github.com/fatih/structs/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Fatih Arslan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
164
Godeps/_workspace/src/github.com/fatih/structs/README.md
generated
vendored
Normal file
164
Godeps/_workspace/src/github.com/fatih/structs/README.md
generated
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
# Structs [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/structs) [![Build Status](http://img.shields.io/travis/fatih/structs.svg?style=flat-square)](https://travis-ci.org/fatih/structs) [![Coverage Status](http://img.shields.io/coveralls/fatih/structs.svg?style=flat-square)](https://coveralls.io/r/fatih/structs)
|
||||
|
||||
Structs contains various utilities to work with Go (Golang) structs. It was
|
||||
initially used by me to convert a struct into a `map[string]interface{}`. With
|
||||
time I've added other utilities for structs. It's basically a high level
|
||||
package based on primitives from the reflect package. Feel free to add new
|
||||
functions or improve the existing code.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/fatih/structs
|
||||
```
|
||||
|
||||
## Usage and Examples
|
||||
|
||||
Just like the standard lib `strings`, `bytes` and co packages, `structs` has
|
||||
many global functions to manipulate or organize your struct data. Lets define
|
||||
and declare a struct:
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ID int
|
||||
Enabled bool
|
||||
users []string // not exported
|
||||
http.Server // embedded
|
||||
}
|
||||
|
||||
server := &Server{
|
||||
Name: "gopher",
|
||||
ID: 123456,
|
||||
Enabled: true,
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// Convert a struct to a map[string]interface{}
|
||||
// => {"Name":"gopher", "ID":123456, "Enabled":true}
|
||||
m := structs.Map(server)
|
||||
|
||||
// Convert the values of a struct to a []interface{}
|
||||
// => ["gopher", 123456, true]
|
||||
v := structs.Values(server)
|
||||
|
||||
// Convert the names of a struct to a []string
|
||||
// (see "Names methods" for more info about fields)
|
||||
n := structs.Names(server)
|
||||
|
||||
// Convert the values of a struct to a []*Field
|
||||
// (see "Field methods" for more info about fields)
|
||||
f := structs.Fields(server)
|
||||
|
||||
// Return the struct name => "Server"
|
||||
n := structs.Name(server)
|
||||
|
||||
// Check if any field of a struct is initialized or not.
|
||||
h := structs.HasZero(server)
|
||||
|
||||
// Check if all fields of a struct is initialized or not.
|
||||
z := structs.IsZero(server)
|
||||
|
||||
// Check if server is a struct or a pointer to struct
|
||||
i := structs.IsStruct(server)
|
||||
```
|
||||
|
||||
### Struct methods
|
||||
|
||||
The structs functions can be also used as independent methods by creating a new
|
||||
`*structs.Struct`. This is handy if you want to have more control over the
|
||||
structs (such as retrieving a single Field).
|
||||
|
||||
```go
|
||||
// Create a new struct type:
|
||||
s := structs.New(server)
|
||||
|
||||
m := s.Map() // Get a map[string]interface{}
|
||||
v := s.Values() // Get a []interface{}
|
||||
f := s.Fields() // Get a []*Field
|
||||
n := s.Names() // Get a []string
|
||||
f := s.Field(name) // Get a *Field based on the given field name
|
||||
f, ok := s.FieldOk(name) // Get a *Field based on the given field name
|
||||
n := s.Name() // Get the struct name
|
||||
h := s.HasZero() // Check if any field is initialized
|
||||
z := s.IsZero() // Check if all fields are initialized
|
||||
```
|
||||
|
||||
### Field methods
|
||||
|
||||
We can easily examine a single Field for more detail. Below you can see how we
|
||||
get and interact with various field methods:
|
||||
|
||||
|
||||
```go
|
||||
s := structs.New(server)
|
||||
|
||||
// Get the Field struct for the "Name" field
|
||||
name := s.Field("Name")
|
||||
|
||||
// Get the underlying value, value => "gopher"
|
||||
value := name.Value().(string)
|
||||
|
||||
// Set the field's value
|
||||
name.Set("another gopher")
|
||||
|
||||
// Get the field's kind, kind => "string"
|
||||
name.Kind()
|
||||
|
||||
// Check if the field is exported or not
|
||||
if name.IsExported() {
|
||||
fmt.Println("Name field is exported")
|
||||
}
|
||||
|
||||
// Check if the value is a zero value, such as "" for string, 0 for int
|
||||
if !name.IsZero() {
|
||||
fmt.Println("Name is initialized")
|
||||
}
|
||||
|
||||
// Check if the field is an anonymous (embedded) field
|
||||
if !name.IsEmbedded() {
|
||||
fmt.Println("Name is not an embedded field")
|
||||
}
|
||||
|
||||
// Get the Field's tag value for tag name "json", tag value => "name,omitempty"
|
||||
tagValue := name.Tag("json")
|
||||
```
|
||||
|
||||
Nested structs are supported too:
|
||||
|
||||
```go
|
||||
addrField := s.Field("Server").Field("Addr")
|
||||
|
||||
// Get the value for addr
|
||||
a := addrField.Value().(string)
|
||||
|
||||
// Or get all fields
|
||||
httpServer := s.Field("Server").Fields()
|
||||
```
|
||||
|
||||
We can also get a slice of Fields from the Struct type to iterate over all
|
||||
fields. This is handy if you wish to examine all fields:
|
||||
|
||||
```go
|
||||
// Convert the fields of a struct to a []*Field
|
||||
fields := s.Fields()
|
||||
|
||||
for _, f := range fields {
|
||||
fmt.Printf("field name: %+v\n", f.Name())
|
||||
|
||||
if f.IsExported() {
|
||||
fmt.Printf("value : %+v\n", f.Value())
|
||||
fmt.Printf("is zero : %+v\n", f.IsZero())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
* [Fatih Arslan](https://github.com/fatih)
|
||||
* [Cihangir Savas](https://github.com/cihangir)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT) - see LICENSE.md for more details
|
126
Godeps/_workspace/src/github.com/fatih/structs/field.go
generated
vendored
Normal file
126
Godeps/_workspace/src/github.com/fatih/structs/field.go
generated
vendored
Normal file
|
@ -0,0 +1,126 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotExported = errors.New("field is not exported")
|
||||
errNotSettable = errors.New("field is not settable")
|
||||
)
|
||||
|
||||
// Field represents a single struct field that encapsulates high level
|
||||
// functions around the field.
|
||||
type Field struct {
|
||||
value reflect.Value
|
||||
field reflect.StructField
|
||||
defaultTag string
|
||||
}
|
||||
|
||||
// Tag returns the value associated with key in the tag string. If there is no
|
||||
// such key in the tag, Tag returns the empty string.
|
||||
func (f *Field) Tag(key string) string {
|
||||
return f.field.Tag.Get(key)
|
||||
}
|
||||
|
||||
// Value returns the underlying value of of the field. It panics if the field
|
||||
// is not exported.
|
||||
func (f *Field) Value() interface{} {
|
||||
return f.value.Interface()
|
||||
}
|
||||
|
||||
// IsEmbedded returns true if the given field is an anonymous field (embedded)
|
||||
func (f *Field) IsEmbedded() bool {
|
||||
return f.field.Anonymous
|
||||
}
|
||||
|
||||
// IsExported returns true if the given field is exported.
|
||||
func (f *Field) IsExported() bool {
|
||||
return f.field.PkgPath == ""
|
||||
}
|
||||
|
||||
// IsZero returns true if the given field is not initalized (has a zero value).
|
||||
// It panics if the field is not exported.
|
||||
func (f *Field) IsZero() bool {
|
||||
zero := reflect.Zero(f.value.Type()).Interface()
|
||||
current := f.Value()
|
||||
|
||||
return reflect.DeepEqual(current, zero)
|
||||
}
|
||||
|
||||
// Name returns the name of the given field
|
||||
func (f *Field) Name() string {
|
||||
return f.field.Name
|
||||
}
|
||||
|
||||
// Kind returns the fields kind, such as "string", "map", "bool", etc ..
|
||||
func (f *Field) Kind() reflect.Kind {
|
||||
return f.value.Kind()
|
||||
}
|
||||
|
||||
// Set sets the field to given value v. It retuns an error if the field is not
|
||||
// settable (not addresable or not exported) or if the given value's type
|
||||
// doesn't match the fields type.
|
||||
func (f *Field) Set(val interface{}) error {
|
||||
// we can't set unexported fields, so be sure this field is exported
|
||||
if !f.IsExported() {
|
||||
return errNotExported
|
||||
}
|
||||
|
||||
// do we get here? not sure...
|
||||
if !f.value.CanSet() {
|
||||
return errNotSettable
|
||||
}
|
||||
|
||||
given := reflect.ValueOf(val)
|
||||
|
||||
if f.value.Kind() != given.Kind() {
|
||||
return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind())
|
||||
}
|
||||
|
||||
f.value.Set(given)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fields returns a slice of Fields. This is particular handy to get the fields
|
||||
// of a nested struct . A struct tag with the content of "-" ignores the
|
||||
// checking of that particular field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field *http.Request `structs:"-"`
|
||||
//
|
||||
// It panics if field is not exported or if field's kind is not struct
|
||||
func (f *Field) Fields() []*Field {
|
||||
return getFields(f.value, f.defaultTag)
|
||||
}
|
||||
|
||||
// Field returns the field from a nested struct. It panics if the nested struct
|
||||
// is not exported or if the field was not found.
|
||||
func (f *Field) Field(name string) *Field {
|
||||
field, ok := f.FieldOk(name)
|
||||
if !ok {
|
||||
panic("field not found")
|
||||
}
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
// Field returns the field from a nested struct. The boolean returns true if
|
||||
// the field was found. It panics if the nested struct is not exported or if
|
||||
// the field was not found.
|
||||
func (f *Field) FieldOk(name string) (*Field, bool) {
|
||||
v := strctVal(f.value.Interface())
|
||||
t := v.Type()
|
||||
|
||||
field, ok := t.FieldByName(name)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &Field{
|
||||
field: field,
|
||||
value: v.FieldByName(name),
|
||||
}, true
|
||||
}
|
324
Godeps/_workspace/src/github.com/fatih/structs/field_test.go
generated
vendored
Normal file
324
Godeps/_workspace/src/github.com/fatih/structs/field_test.go
generated
vendored
Normal file
|
@ -0,0 +1,324 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A test struct that defines all cases
|
||||
type Foo struct {
|
||||
A string
|
||||
B int `structs:"y"`
|
||||
C bool `json:"c"`
|
||||
d string // not exported
|
||||
E *Baz
|
||||
x string `xml:"x"` // not exported, with tag
|
||||
Y []string
|
||||
Z map[string]interface{}
|
||||
*Bar // embedded
|
||||
}
|
||||
|
||||
type Baz struct {
|
||||
A string
|
||||
B int
|
||||
}
|
||||
|
||||
type Bar struct {
|
||||
E string
|
||||
F int
|
||||
g []string
|
||||
}
|
||||
|
||||
func newStruct() *Struct {
|
||||
b := &Bar{
|
||||
E: "example",
|
||||
F: 2,
|
||||
g: []string{"zeynep", "fatih"},
|
||||
}
|
||||
|
||||
// B and x is not initialized for testing
|
||||
f := &Foo{
|
||||
A: "gopher",
|
||||
C: true,
|
||||
d: "small",
|
||||
E: nil,
|
||||
Y: []string{"example"},
|
||||
Z: nil,
|
||||
}
|
||||
f.Bar = b
|
||||
|
||||
return New(f)
|
||||
}
|
||||
|
||||
func TestField_Set(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
f := s.Field("A")
|
||||
err := f.Set("fatih")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if f.Value().(string) != "fatih" {
|
||||
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih")
|
||||
}
|
||||
|
||||
f = s.Field("Y")
|
||||
err = f.Set([]string{"override", "with", "this"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
sliceLen := len(f.Value().([]string))
|
||||
if sliceLen != 3 {
|
||||
t.Errorf("Setted values slice length is wrong: %d, want: %d", sliceLen, 3)
|
||||
}
|
||||
|
||||
f = s.Field("C")
|
||||
err = f.Set(false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if f.Value().(bool) {
|
||||
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(bool), false)
|
||||
}
|
||||
|
||||
// let's pass a different type
|
||||
f = s.Field("A")
|
||||
err = f.Set(123) // Field A is of type string, but we are going to pass an integer
|
||||
if err == nil {
|
||||
t.Error("Setting a field's value with a different type than the field's type should return an error")
|
||||
}
|
||||
|
||||
// old value should be still there :)
|
||||
if f.Value().(string) != "fatih" {
|
||||
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih")
|
||||
}
|
||||
|
||||
// let's access an unexported field, which should give an error
|
||||
f = s.Field("d")
|
||||
err = f.Set("large")
|
||||
if err != errNotExported {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// let's set a pointer to struct
|
||||
b := &Bar{
|
||||
E: "gopher",
|
||||
F: 2,
|
||||
}
|
||||
|
||||
f = s.Field("Bar")
|
||||
err = f.Set(b)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
baz := &Baz{
|
||||
A: "helloWorld",
|
||||
B: 42,
|
||||
}
|
||||
|
||||
f = s.Field("E")
|
||||
err = f.Set(baz)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ba := s.Field("E").Value().(*Baz)
|
||||
|
||||
if ba.A != "helloWorld" {
|
||||
t.Errorf("could not set baz. Got: %s Want: helloWorld", ba.A)
|
||||
}
|
||||
}
|
||||
|
||||
func TestField(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
t.Error("Retrieveing a non existing field from the struct should panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_ = s.Field("no-field")
|
||||
}
|
||||
|
||||
func TestField_Kind(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
f := s.Field("A")
|
||||
if f.Kind() != reflect.String {
|
||||
t.Errorf("Field A has wrong kind: %s want: %s", f.Kind(), reflect.String)
|
||||
}
|
||||
|
||||
f = s.Field("B")
|
||||
if f.Kind() != reflect.Int {
|
||||
t.Errorf("Field B has wrong kind: %s want: %s", f.Kind(), reflect.Int)
|
||||
}
|
||||
|
||||
// unexported
|
||||
f = s.Field("d")
|
||||
if f.Kind() != reflect.String {
|
||||
t.Errorf("Field d has wrong kind: %s want: %s", f.Kind(), reflect.String)
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_Tag(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
v := s.Field("B").Tag("json")
|
||||
if v != "" {
|
||||
t.Errorf("Field's tag value of a non existing tag should return empty, got: %s", v)
|
||||
}
|
||||
|
||||
v = s.Field("C").Tag("json")
|
||||
if v != "c" {
|
||||
t.Errorf("Field's tag value of the existing field C should return 'c', got: %s", v)
|
||||
}
|
||||
|
||||
v = s.Field("d").Tag("json")
|
||||
if v != "" {
|
||||
t.Errorf("Field's tag value of a non exported field should return empty, got: %s", v)
|
||||
}
|
||||
|
||||
v = s.Field("x").Tag("xml")
|
||||
if v != "x" {
|
||||
t.Errorf("Field's tag value of a non exported field with a tag should return 'x', got: %s", v)
|
||||
}
|
||||
|
||||
v = s.Field("A").Tag("json")
|
||||
if v != "" {
|
||||
t.Errorf("Field's tag value of a existing field without a tag should return empty, got: %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_Value(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
v := s.Field("A").Value()
|
||||
val, ok := v.(string)
|
||||
if !ok {
|
||||
t.Errorf("Field's value of a A should be string")
|
||||
}
|
||||
|
||||
if val != "gopher" {
|
||||
t.Errorf("Field's value of a existing tag should return 'gopher', got: %s", val)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
t.Error("Value of a non exported field from the field should panic")
|
||||
}
|
||||
}()
|
||||
|
||||
// should panic
|
||||
_ = s.Field("d").Value()
|
||||
}
|
||||
|
||||
func TestField_IsEmbedded(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
if !s.Field("Bar").IsEmbedded() {
|
||||
t.Errorf("Fields 'Bar' field is an embedded field")
|
||||
}
|
||||
|
||||
if s.Field("d").IsEmbedded() {
|
||||
t.Errorf("Fields 'd' field is not an embedded field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_IsExported(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
if !s.Field("Bar").IsExported() {
|
||||
t.Errorf("Fields 'Bar' field is an exported field")
|
||||
}
|
||||
|
||||
if !s.Field("A").IsExported() {
|
||||
t.Errorf("Fields 'A' field is an exported field")
|
||||
}
|
||||
|
||||
if s.Field("d").IsExported() {
|
||||
t.Errorf("Fields 'd' field is not an exported field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_IsZero(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
if s.Field("A").IsZero() {
|
||||
t.Errorf("Fields 'A' field is an initialized field")
|
||||
}
|
||||
|
||||
if !s.Field("B").IsZero() {
|
||||
t.Errorf("Fields 'B' field is not an initialized field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_Name(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
if s.Field("A").Name() != "A" {
|
||||
t.Errorf("Fields 'A' field should have the name 'A'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_Field(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
e := s.Field("Bar").Field("E")
|
||||
|
||||
val, ok := e.Value().(string)
|
||||
if !ok {
|
||||
t.Error("The value of the field 'e' inside 'Bar' struct should be string")
|
||||
}
|
||||
|
||||
if val != "example" {
|
||||
t.Errorf("The value of 'e' should be 'example, got: %s", val)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
t.Error("Field of a non existing nested struct should panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_ = s.Field("Bar").Field("e")
|
||||
}
|
||||
|
||||
func TestField_Fields(t *testing.T) {
|
||||
s := newStruct()
|
||||
fields := s.Field("Bar").Fields()
|
||||
|
||||
if len(fields) != 3 {
|
||||
t.Errorf("We expect 3 fields in embedded struct, was: %d", len(fields))
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_FieldOk(t *testing.T) {
|
||||
s := newStruct()
|
||||
|
||||
b, ok := s.FieldOk("Bar")
|
||||
if !ok {
|
||||
t.Error("The field 'Bar' should exists.")
|
||||
}
|
||||
|
||||
e, ok := b.FieldOk("E")
|
||||
if !ok {
|
||||
t.Error("The field 'E' should exists.")
|
||||
}
|
||||
|
||||
val, ok := e.Value().(string)
|
||||
if !ok {
|
||||
t.Error("The value of the field 'e' inside 'Bar' struct should be string")
|
||||
}
|
||||
|
||||
if val != "example" {
|
||||
t.Errorf("The value of 'e' should be 'example, got: %s", val)
|
||||
}
|
||||
}
|
449
Godeps/_workspace/src/github.com/fatih/structs/structs.go
generated
vendored
Normal file
449
Godeps/_workspace/src/github.com/fatih/structs/structs.go
generated
vendored
Normal file
|
@ -0,0 +1,449 @@
|
|||
// Package structs contains various utilities functions to work with structs.
|
||||
package structs
|
||||
|
||||
import "reflect"
|
||||
|
||||
var (
|
||||
// DefaultTagName is the default tag name for struct fields which provides
|
||||
// a more granular to tweak certain structs. Lookup the necessary functions
|
||||
// for more info.
|
||||
DefaultTagName = "structs" // struct's field default tag name
|
||||
)
|
||||
|
||||
// Struct encapsulates a struct type to provide several high level functions
|
||||
// around the struct.
|
||||
type Struct struct {
|
||||
raw interface{}
|
||||
value reflect.Value
|
||||
TagName string
|
||||
}
|
||||
|
||||
// New returns a new *Struct with the struct s. It panics if the s's kind is
|
||||
// not struct.
|
||||
func New(s interface{}) *Struct {
|
||||
return &Struct{
|
||||
raw: s,
|
||||
value: strctVal(s),
|
||||
TagName: DefaultTagName,
|
||||
}
|
||||
}
|
||||
|
||||
// Map converts the given struct to a map[string]interface{}, where the keys
|
||||
// of the map are the field names and the values of the map the associated
|
||||
// values of the fields. The default key string is the struct field name but
|
||||
// can be changed in the struct field's tag value. The "structs" key in the
|
||||
// struct's field tag value is the key name. Example:
|
||||
//
|
||||
// // Field appears in map as key "myName".
|
||||
// Name string `structs:"myName"`
|
||||
//
|
||||
// A tag value with the content of "-" ignores that particular field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field bool `structs:"-"`
|
||||
//
|
||||
// A tag value with the option of "omitnested" stops iterating further if the type
|
||||
// is a struct. Example:
|
||||
//
|
||||
// // Field is not processed further by this package.
|
||||
// Field time.Time `structs:"myName,omitnested"`
|
||||
// Field *http.Request `structs:",omitnested"`
|
||||
//
|
||||
// A tag value with the option of "omitempty" ignores that particular field if
|
||||
// the field value is empty. Example:
|
||||
//
|
||||
// // Field appears in map as key "myName", but the field is
|
||||
// // skipped if empty.
|
||||
// Field string `structs:"myName,omitempty"`
|
||||
//
|
||||
// // Field appears in map as key "Field" (the default), but
|
||||
// // the field is skipped if empty.
|
||||
// Field string `structs:",omitempty"`
|
||||
//
|
||||
// Note that only exported fields of a struct can be accessed, non exported
|
||||
// fields will be neglected.
|
||||
func (s *Struct) Map() map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
|
||||
fields := s.structFields()
|
||||
|
||||
for _, field := range fields {
|
||||
name := field.Name
|
||||
val := s.value.FieldByName(name)
|
||||
|
||||
var finalVal interface{}
|
||||
|
||||
tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||
if tagName != "" {
|
||||
name = tagName
|
||||
}
|
||||
|
||||
// if the value is a zero value and the field is marked as omitempty do
|
||||
// not include
|
||||
if tagOpts.Has("omitempty") {
|
||||
zero := reflect.Zero(val.Type()).Interface()
|
||||
current := val.Interface()
|
||||
|
||||
if reflect.DeepEqual(current, zero) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||
// look out for embedded structs, and convert them to a
|
||||
// map[string]interface{} too
|
||||
n := New(val.Interface())
|
||||
n.TagName = s.TagName
|
||||
finalVal = n.Map()
|
||||
} else {
|
||||
finalVal = val.Interface()
|
||||
}
|
||||
|
||||
out[name] = finalVal
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Values converts the given s struct's field values to a []interface{}. A
|
||||
// struct tag with the content of "-" ignores the that particular field.
|
||||
// Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field int `structs:"-"`
|
||||
//
|
||||
// A value with the option of "omitnested" stops iterating further if the type
|
||||
// is a struct. Example:
|
||||
//
|
||||
// // Fields is not processed further by this package.
|
||||
// Field time.Time `structs:",omitnested"`
|
||||
// Field *http.Request `structs:",omitnested"`
|
||||
//
|
||||
// A tag value with the option of "omitempty" ignores that particular field and
|
||||
// is not added to the values if the field value is empty. Example:
|
||||
//
|
||||
// // Field is skipped if empty
|
||||
// Field string `structs:",omitempty"`
|
||||
//
|
||||
// Note that only exported fields of a struct can be accessed, non exported
|
||||
// fields will be neglected.
|
||||
func (s *Struct) Values() []interface{} {
|
||||
fields := s.structFields()
|
||||
|
||||
var t []interface{}
|
||||
|
||||
for _, field := range fields {
|
||||
val := s.value.FieldByName(field.Name)
|
||||
|
||||
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||
|
||||
// if the value is a zero value and the field is marked as omitempty do
|
||||
// not include
|
||||
if tagOpts.Has("omitempty") {
|
||||
zero := reflect.Zero(val.Type()).Interface()
|
||||
current := val.Interface()
|
||||
|
||||
if reflect.DeepEqual(current, zero) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||
// look out for embedded structs, and convert them to a
|
||||
// []interface{} to be added to the final values slice
|
||||
for _, embeddedVal := range Values(val.Interface()) {
|
||||
t = append(t, embeddedVal)
|
||||
}
|
||||
} else {
|
||||
t = append(t, val.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Fields returns a slice of Fields. A struct tag with the content of "-"
|
||||
// ignores the checking of that particular field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field bool `structs:"-"`
|
||||
//
|
||||
// It panics if s's kind is not struct.
|
||||
func (s *Struct) Fields() []*Field {
|
||||
return getFields(s.value, s.TagName)
|
||||
}
|
||||
|
||||
// Names returns a slice of field names. A struct tag with the content of "-"
|
||||
// ignores the checking of that particular field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field bool `structs:"-"`
|
||||
//
|
||||
// It panics if s's kind is not struct.
|
||||
func (s *Struct) Names() []string {
|
||||
fields := getFields(s.value, s.TagName)
|
||||
|
||||
names := make([]string, len(fields))
|
||||
|
||||
for i, field := range fields {
|
||||
names[i] = field.Name()
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func getFields(v reflect.Value, tagName string) []*Field {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
|
||||
var fields []*Field
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
if tag := field.Tag.Get(tagName); tag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
f := &Field{
|
||||
field: field,
|
||||
value: v.FieldByName(field.Name),
|
||||
}
|
||||
|
||||
fields = append(fields, f)
|
||||
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// Field returns a new Field struct that provides several high level functions
|
||||
// around a single struct field entity. It panics if the field is not found.
|
||||
func (s *Struct) Field(name string) *Field {
|
||||
f, ok := s.FieldOk(name)
|
||||
if !ok {
|
||||
panic("field not found")
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// Field returns a new Field struct that provides several high level functions
|
||||
// around a single struct field entity. The boolean returns true if the field
|
||||
// was found.
|
||||
func (s *Struct) FieldOk(name string) (*Field, bool) {
|
||||
t := s.value.Type()
|
||||
|
||||
field, ok := t.FieldByName(name)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &Field{
|
||||
field: field,
|
||||
value: s.value.FieldByName(name),
|
||||
defaultTag: s.TagName,
|
||||
}, true
|
||||
}
|
||||
|
||||
// IsZero returns true if all fields in a struct is a zero value (not
|
||||
// initialized) A struct tag with the content of "-" ignores the checking of
|
||||
// that particular field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field bool `structs:"-"`
|
||||
//
|
||||
// A value with the option of "omitnested" stops iterating further if the type
|
||||
// is a struct. Example:
|
||||
//
|
||||
// // Field is not processed further by this package.
|
||||
// Field time.Time `structs:"myName,omitnested"`
|
||||
// Field *http.Request `structs:",omitnested"`
|
||||
//
|
||||
// Note that only exported fields of a struct can be accessed, non exported
|
||||
// fields will be neglected. It panics if s's kind is not struct.
|
||||
func (s *Struct) IsZero() bool {
|
||||
fields := s.structFields()
|
||||
|
||||
for _, field := range fields {
|
||||
val := s.value.FieldByName(field.Name)
|
||||
|
||||
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||
|
||||
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||
ok := IsZero(val.Interface())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// zero value of the given field, such as "" for string, 0 for int
|
||||
zero := reflect.Zero(val.Type()).Interface()
|
||||
|
||||
// current value of the given field
|
||||
current := val.Interface()
|
||||
|
||||
if !reflect.DeepEqual(current, zero) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// HasZero returns true if a field in a struct is not initialized (zero value).
|
||||
// A struct tag with the content of "-" ignores the checking of that particular
|
||||
// field. Example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field bool `structs:"-"`
|
||||
//
|
||||
// A value with the option of "omitnested" stops iterating further if the type
|
||||
// is a struct. Example:
|
||||
//
|
||||
// // Field is not processed further by this package.
|
||||
// Field time.Time `structs:"myName,omitnested"`
|
||||
// Field *http.Request `structs:",omitnested"`
|
||||
//
|
||||
// Note that only exported fields of a struct can be accessed, non exported
|
||||
// fields will be neglected. It panics if s's kind is not struct.
|
||||
func (s *Struct) HasZero() bool {
|
||||
fields := s.structFields()
|
||||
|
||||
for _, field := range fields {
|
||||
val := s.value.FieldByName(field.Name)
|
||||
|
||||
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||
|
||||
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||
ok := HasZero(val.Interface())
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// zero value of the given field, such as "" for string, 0 for int
|
||||
zero := reflect.Zero(val.Type()).Interface()
|
||||
|
||||
// current value of the given field
|
||||
current := val.Interface()
|
||||
|
||||
if reflect.DeepEqual(current, zero) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Name returns the structs's type name within its package. For more info refer
|
||||
// to Name() function.
|
||||
func (s *Struct) Name() string {
|
||||
return s.value.Type().Name()
|
||||
}
|
||||
|
||||
// structFields returns the exported struct fields for a given s struct. This
|
||||
// is a convenient helper method to avoid duplicate code in some of the
|
||||
// functions.
|
||||
func (s *Struct) structFields() []reflect.StructField {
|
||||
t := s.value.Type()
|
||||
|
||||
var f []reflect.StructField
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
// we can't access the value of unexported fields
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// don't check if it's omitted
|
||||
if tag := field.Tag.Get(s.TagName); tag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
f = append(f, field)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func strctVal(s interface{}) reflect.Value {
|
||||
v := reflect.ValueOf(s)
|
||||
|
||||
// if pointer get the underlying element≤
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Struct {
|
||||
panic("not struct")
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Map converts the given struct to a map[string]interface{}. For more info
|
||||
// refer to Struct types Map() method. It panics if s's kind is not struct.
|
||||
func Map(s interface{}) map[string]interface{} {
|
||||
return New(s).Map()
|
||||
}
|
||||
|
||||
// Values converts the given struct to a []interface{}. For more info refer to
|
||||
// Struct types Values() method. It panics if s's kind is not struct.
|
||||
func Values(s interface{}) []interface{} {
|
||||
return New(s).Values()
|
||||
}
|
||||
|
||||
// Fields returns a slice of *Field. For more info refer to Struct types
|
||||
// Fields() method. It panics if s's kind is not struct.
|
||||
func Fields(s interface{}) []*Field {
|
||||
return New(s).Fields()
|
||||
}
|
||||
|
||||
// Names returns a slice of field names. For more info refer to Struct types
|
||||
// Names() method. It panics if s's kind is not struct.
|
||||
func Names(s interface{}) []string {
|
||||
return New(s).Names()
|
||||
}
|
||||
|
||||
// IsZero returns true if all fields is equal to a zero value. For more info
|
||||
// refer to Struct types IsZero() method. It panics if s's kind is not struct.
|
||||
func IsZero(s interface{}) bool {
|
||||
return New(s).IsZero()
|
||||
}
|
||||
|
||||
// HasZero returns true if any field is equal to a zero value. For more info
|
||||
// refer to Struct types HasZero() method. It panics if s's kind is not struct.
|
||||
func HasZero(s interface{}) bool {
|
||||
return New(s).HasZero()
|
||||
}
|
||||
|
||||
// IsStruct returns true if the given variable is a struct or a pointer to
|
||||
// struct.
|
||||
func IsStruct(s interface{}) bool {
|
||||
v := reflect.ValueOf(s)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// uninitialized zero value of a struct
|
||||
if v.Kind() == reflect.Invalid {
|
||||
return false
|
||||
}
|
||||
|
||||
return v.Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// Name returns the structs's type name within its package. It returns an
|
||||
// empty string for unnamed types. It panics if s's kind is not struct.
|
||||
func Name(s interface{}) string {
|
||||
return New(s).Name()
|
||||
}
|
351
Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go
generated
vendored
Normal file
351
Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go
generated
vendored
Normal file
|
@ -0,0 +1,351 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
type Server struct {
|
||||
Name string
|
||||
ID int32
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
server := &Server{
|
||||
Name: "Arslan",
|
||||
ID: 123456,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
s := New(server)
|
||||
|
||||
fmt.Printf("Name : %v\n", s.Name())
|
||||
fmt.Printf("Values : %v\n", s.Values())
|
||||
fmt.Printf("Value of ID : %v\n", s.Field("ID").Value())
|
||||
// Output:
|
||||
// Name : Server
|
||||
// Values : [Arslan 123456 true]
|
||||
// Value of ID : 123456
|
||||
|
||||
}
|
||||
|
||||
func ExampleMap() {
|
||||
type Server struct {
|
||||
Name string
|
||||
ID int32
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Name: "Arslan",
|
||||
ID: 123456,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
m := Map(s)
|
||||
|
||||
fmt.Printf("%#v\n", m["Name"])
|
||||
fmt.Printf("%#v\n", m["ID"])
|
||||
fmt.Printf("%#v\n", m["Enabled"])
|
||||
// Output:
|
||||
// "Arslan"
|
||||
// 123456
|
||||
// true
|
||||
|
||||
}
|
||||
|
||||
func ExampleMap_tags() {
|
||||
// Custom tags can change the map keys instead of using the fields name
|
||||
type Server struct {
|
||||
Name string `structs:"server_name"`
|
||||
ID int32 `structs:"server_id"`
|
||||
Enabled bool `structs:"enabled"`
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Name: "Zeynep",
|
||||
ID: 789012,
|
||||
}
|
||||
|
||||
m := Map(s)
|
||||
|
||||
// access them by the custom tags defined above
|
||||
fmt.Printf("%#v\n", m["server_name"])
|
||||
fmt.Printf("%#v\n", m["server_id"])
|
||||
fmt.Printf("%#v\n", m["enabled"])
|
||||
// Output:
|
||||
// "Zeynep"
|
||||
// 789012
|
||||
// false
|
||||
|
||||
}
|
||||
|
||||
func ExampleMap_nested() {
|
||||
// By default field with struct types are processed too. We can stop
|
||||
// processing them via "omitnested" tag option.
|
||||
type Server struct {
|
||||
Name string `structs:"server_name"`
|
||||
ID int32 `structs:"server_id"`
|
||||
Time time.Time `structs:"time,omitnested"` // do not convert to map[string]interface{}
|
||||
}
|
||||
|
||||
const shortForm = "2006-Jan-02"
|
||||
t, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
|
||||
|
||||
s := &Server{
|
||||
Name: "Zeynep",
|
||||
ID: 789012,
|
||||
Time: t,
|
||||
}
|
||||
|
||||
m := Map(s)
|
||||
|
||||
// access them by the custom tags defined above
|
||||
fmt.Printf("%v\n", m["server_name"])
|
||||
fmt.Printf("%v\n", m["server_id"])
|
||||
fmt.Printf("%v\n", m["time"].(time.Time))
|
||||
// Output:
|
||||
// Zeynep
|
||||
// 789012
|
||||
// 2013-02-03 00:00:00 +0000 UTC
|
||||
}
|
||||
|
||||
func ExampleMap_omitEmpty() {
|
||||
// By default field with struct types of zero values are processed too. We
|
||||
// can stop processing them via "omitempty" tag option.
|
||||
type Server struct {
|
||||
Name string `structs:",omitempty"`
|
||||
ID int32 `structs:"server_id,omitempty"`
|
||||
Location string
|
||||
}
|
||||
|
||||
// Only add location
|
||||
s := &Server{
|
||||
Location: "Tokyo",
|
||||
}
|
||||
|
||||
m := Map(s)
|
||||
|
||||
// map contains only the Location field
|
||||
fmt.Printf("%v\n", m)
|
||||
// Output:
|
||||
// map[Location:Tokyo]
|
||||
}
|
||||
|
||||
func ExampleValues() {
|
||||
type Server struct {
|
||||
Name string
|
||||
ID int32
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Name: "Fatih",
|
||||
ID: 135790,
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
m := Values(s)
|
||||
|
||||
fmt.Printf("Values: %+v\n", m)
|
||||
// Output:
|
||||
// Values: [Fatih 135790 false]
|
||||
}
|
||||
|
||||
func ExampleValues_omitEmpty() {
|
||||
// By default field with struct types of zero values are processed too. We
|
||||
// can stop processing them via "omitempty" tag option.
|
||||
type Server struct {
|
||||
Name string `structs:",omitempty"`
|
||||
ID int32 `structs:"server_id,omitempty"`
|
||||
Location string
|
||||
}
|
||||
|
||||
// Only add location
|
||||
s := &Server{
|
||||
Location: "Ankara",
|
||||
}
|
||||
|
||||
m := Values(s)
|
||||
|
||||
// values contains only the Location field
|
||||
fmt.Printf("Values: %+v\n", m)
|
||||
// Output:
|
||||
// Values: [Ankara]
|
||||
}
|
||||
|
||||
func ExampleValues_tags() {
|
||||
type Location struct {
|
||||
City string
|
||||
Country string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Name string
|
||||
ID int32
|
||||
Enabled bool
|
||||
Location Location `structs:"-"` // values from location are not included anymore
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Name: "Fatih",
|
||||
ID: 135790,
|
||||
Enabled: false,
|
||||
Location: Location{City: "Ankara", Country: "Turkey"},
|
||||
}
|
||||
|
||||
// Let get all values from the struct s. Note that we don't include values
|
||||
// from the Location field
|
||||
m := Values(s)
|
||||
|
||||
fmt.Printf("Values: %+v\n", m)
|
||||
// Output:
|
||||
// Values: [Fatih 135790 false]
|
||||
}
|
||||
|
||||
func ExampleFields() {
|
||||
type Access struct {
|
||||
Name string
|
||||
LastAccessed time.Time
|
||||
Number int
|
||||
}
|
||||
|
||||
s := &Access{
|
||||
Name: "Fatih",
|
||||
LastAccessed: time.Now(),
|
||||
Number: 1234567,
|
||||
}
|
||||
|
||||
fields := Fields(s)
|
||||
|
||||
for i, field := range fields {
|
||||
fmt.Printf("[%d] %+v\n", i, field.Name())
|
||||
}
|
||||
|
||||
// Output:
|
||||
// [0] Name
|
||||
// [1] LastAccessed
|
||||
// [2] Number
|
||||
}
|
||||
|
||||
func ExampleFields_nested() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Number int
|
||||
}
|
||||
|
||||
type Access struct {
|
||||
Person Person
|
||||
HasPermission bool
|
||||
LastAccessed time.Time
|
||||
}
|
||||
|
||||
s := &Access{
|
||||
Person: Person{Name: "fatih", Number: 1234567},
|
||||
LastAccessed: time.Now(),
|
||||
HasPermission: true,
|
||||
}
|
||||
|
||||
// Let's get all fields from the struct s.
|
||||
fields := Fields(s)
|
||||
|
||||
for _, field := range fields {
|
||||
if field.Name() == "Person" {
|
||||
fmt.Printf("Access.Person.Name: %+v\n", field.Field("Name").Value())
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Access.Person.Name: fatih
|
||||
}
|
||||
|
||||
func ExampleField() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Number int
|
||||
}
|
||||
|
||||
type Access struct {
|
||||
Person Person
|
||||
HasPermission bool
|
||||
LastAccessed time.Time
|
||||
}
|
||||
|
||||
access := &Access{
|
||||
Person: Person{Name: "fatih", Number: 1234567},
|
||||
LastAccessed: time.Now(),
|
||||
HasPermission: true,
|
||||
}
|
||||
|
||||
// Create a new Struct type
|
||||
s := New(access)
|
||||
|
||||
// Get the Field type for "Person" field
|
||||
p := s.Field("Person")
|
||||
|
||||
// Get the underlying "Name field" and print the value of it
|
||||
name := p.Field("Name")
|
||||
|
||||
fmt.Printf("Value of Person.Access.Name: %+v\n", name.Value())
|
||||
|
||||
// Output:
|
||||
// Value of Person.Access.Name: fatih
|
||||
|
||||
}
|
||||
|
||||
func ExampleIsZero() {
|
||||
type Server struct {
|
||||
Name string
|
||||
ID int32
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// Nothing is initalized
|
||||
a := &Server{}
|
||||
isZeroA := IsZero(a)
|
||||
|
||||
// Name and Enabled is initialized, but not ID
|
||||
b := &Server{
|
||||
Name: "Golang",
|
||||
Enabled: true,
|
||||
}
|
||||
isZeroB := IsZero(b)
|
||||
|
||||
fmt.Printf("%#v\n", isZeroA)
|
||||
fmt.Printf("%#v\n", isZeroB)
|
||||
// Output:
|
||||
// true
|
||||
// false
|
||||
}
|
||||
|
||||
func ExampleHasZero() {
|
||||
// Let's define an Access struct. Note that the "Enabled" field is not
|
||||
// going to be checked because we added the "structs" tag to the field.
|
||||
type Access struct {
|
||||
Name string
|
||||
LastAccessed time.Time
|
||||
Number int
|
||||
Enabled bool `structs:"-"`
|
||||
}
|
||||
|
||||
// Name and Number is not initialized.
|
||||
a := &Access{
|
||||
LastAccessed: time.Now(),
|
||||
}
|
||||
hasZeroA := HasZero(a)
|
||||
|
||||
// Name and Number is initialized.
|
||||
b := &Access{
|
||||
Name: "Fatih",
|
||||
LastAccessed: time.Now(),
|
||||
Number: 12345,
|
||||
}
|
||||
hasZeroB := HasZero(b)
|
||||
|
||||
fmt.Printf("%#v\n", hasZeroA)
|
||||
fmt.Printf("%#v\n", hasZeroB)
|
||||
// Output:
|
||||
// true
|
||||
// false
|
||||
}
|
898
Godeps/_workspace/src/github.com/fatih/structs/structs_test.go
generated
vendored
Normal file
898
Godeps/_workspace/src/github.com/fatih/structs/structs_test.go
generated
vendored
Normal file
|
@ -0,0 +1,898 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMapNonStruct(t *testing.T) {
|
||||
foo := []string{"foo"}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
t.Error("Passing a non struct into Map should panic")
|
||||
}
|
||||
}()
|
||||
|
||||
// this should panic. We are going to recover and and test it
|
||||
_ = Map(foo)
|
||||
}
|
||||
|
||||
func TestStructIndexes(t *testing.T) {
|
||||
type C struct {
|
||||
something int
|
||||
Props map[string]interface{}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
fmt.Printf("err %+v\n", err)
|
||||
t.Error("Using mixed indexes should not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
// They should not panic
|
||||
_ = Map(&C{})
|
||||
_ = Fields(&C{})
|
||||
_ = Values(&C{})
|
||||
_ = IsZero(&C{})
|
||||
_ = HasZero(&C{})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
|
||||
a := Map(T)
|
||||
|
||||
if typ := reflect.TypeOf(a).Kind(); typ != reflect.Map {
|
||||
t.Errorf("Map should return a map type, got: %v", typ)
|
||||
}
|
||||
|
||||
// we have three fields
|
||||
if len(a) != 3 {
|
||||
t.Errorf("Map should return a map of len 3, got: %d", len(a))
|
||||
}
|
||||
|
||||
inMap := func(val interface{}) bool {
|
||||
for _, v := range a {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"a-value", 2, true} {
|
||||
if !inMap(val) {
|
||||
t.Errorf("Map should have the value %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMap_Tag(t *testing.T) {
|
||||
var T = struct {
|
||||
A string `structs:"x"`
|
||||
B int `structs:"y"`
|
||||
C bool `structs:"z"`
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
|
||||
a := Map(T)
|
||||
|
||||
inMap := func(key interface{}) bool {
|
||||
for k := range a {
|
||||
if reflect.DeepEqual(k, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, key := range []string{"x", "y", "z"} {
|
||||
if !inMap(key) {
|
||||
t.Errorf("Map should have the key %v", key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMap_CustomTag(t *testing.T) {
|
||||
var T = struct {
|
||||
A string `json:"x"`
|
||||
B int `json:"y"`
|
||||
C bool `json:"z"`
|
||||
D struct {
|
||||
E string `json:"jkl"`
|
||||
} `json:"nested"`
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
T.D.E = "e-value"
|
||||
|
||||
s := New(T)
|
||||
s.TagName = "json"
|
||||
|
||||
a := s.Map()
|
||||
|
||||
inMap := func(key interface{}) bool {
|
||||
for k := range a {
|
||||
if reflect.DeepEqual(k, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, key := range []string{"x", "y", "z"} {
|
||||
if !inMap(key) {
|
||||
t.Errorf("Map should have the key %v", key)
|
||||
}
|
||||
}
|
||||
|
||||
nested, ok := a["nested"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Map should contain the D field that is tagged as 'nested'")
|
||||
}
|
||||
|
||||
e, ok := nested["jkl"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("Map should contain the D.E field that is tagged as 'jkl'")
|
||||
}
|
||||
|
||||
if e != "e-value" {
|
||||
t.Errorf("D.E field should be equal to 'e-value', got: '%v'", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMap_MultipleCustomTag(t *testing.T) {
|
||||
var A = struct {
|
||||
X string `aa:"ax"`
|
||||
}{"a_value"}
|
||||
|
||||
aStruct := New(A)
|
||||
aStruct.TagName = "aa"
|
||||
|
||||
var B = struct {
|
||||
X string `bb:"bx"`
|
||||
}{"b_value"}
|
||||
|
||||
bStruct := New(B)
|
||||
bStruct.TagName = "bb"
|
||||
|
||||
a, b := aStruct.Map(), bStruct.Map()
|
||||
if !reflect.DeepEqual(a, map[string]interface{}{"ax": "a_value"}) {
|
||||
t.Error("Map should have field ax with value a_value")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(b, map[string]interface{}{"bx": "b_value"}) {
|
||||
t.Error("Map should have field bx with value b_value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_OmitEmpty(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
Value string `structs:",omitempty"`
|
||||
Time time.Time `structs:",omitempty"`
|
||||
}
|
||||
a := A{}
|
||||
|
||||
m := Map(a)
|
||||
|
||||
_, ok := m["Value"].(map[string]interface{})
|
||||
if ok {
|
||||
t.Error("Map should not contain the Value field that is tagged as omitempty")
|
||||
}
|
||||
|
||||
_, ok = m["Time"].(map[string]interface{})
|
||||
if ok {
|
||||
t.Error("Map should not contain the Time field that is tagged as omitempty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_OmitNested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
Value string
|
||||
Time time.Time `structs:",omitnested"`
|
||||
}
|
||||
a := A{Time: time.Now()}
|
||||
|
||||
type B struct {
|
||||
Desc string
|
||||
A A
|
||||
}
|
||||
b := &B{A: a}
|
||||
|
||||
m := Map(b)
|
||||
|
||||
in, ok := m["A"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("Map nested structs is not available in the map")
|
||||
}
|
||||
|
||||
// should not happen
|
||||
if _, ok := in["Time"].(map[string]interface{}); ok {
|
||||
t.Error("Map nested struct should omit recursiving parsing of Time")
|
||||
}
|
||||
|
||||
if _, ok := in["Time"].(time.Time); !ok {
|
||||
t.Error("Map nested struct should stop parsing of Time at is current value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_Nested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
}
|
||||
a := &A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A *A
|
||||
}
|
||||
b := &B{A: a}
|
||||
|
||||
m := Map(b)
|
||||
|
||||
if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map {
|
||||
t.Errorf("Map should return a map type, got: %v", typ)
|
||||
}
|
||||
|
||||
in, ok := m["A"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("Map nested structs is not available in the map")
|
||||
}
|
||||
|
||||
if name := in["Name"].(string); name != "example" {
|
||||
t.Errorf("Map nested struct's name field should give example, got: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_Anonymous(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
}
|
||||
a := &A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
*A
|
||||
}
|
||||
b := &B{}
|
||||
b.A = a
|
||||
|
||||
m := Map(b)
|
||||
|
||||
if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map {
|
||||
t.Errorf("Map should return a map type, got: %v", typ)
|
||||
}
|
||||
|
||||
in, ok := m["A"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("Embedded structs is not available in the map")
|
||||
}
|
||||
|
||||
if name := in["Name"].(string); name != "example" {
|
||||
t.Errorf("Embedded A struct's Name field should give example, got: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStruct(t *testing.T) {
|
||||
var T = struct{}{}
|
||||
|
||||
if !IsStruct(T) {
|
||||
t.Errorf("T should be a struct, got: %T", T)
|
||||
}
|
||||
|
||||
if !IsStruct(&T) {
|
||||
t.Errorf("T should be a struct, got: %T", T)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
|
||||
s := Values(T)
|
||||
|
||||
if typ := reflect.TypeOf(s).Kind(); typ != reflect.Slice {
|
||||
t.Errorf("Values should return a slice type, got: %v", typ)
|
||||
}
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"a-value", 2, true} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Values should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues_OmitEmpty(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
Value int `structs:",omitempty"`
|
||||
}
|
||||
|
||||
a := A{Name: "example"}
|
||||
s := Values(a)
|
||||
|
||||
if len(s) != 1 {
|
||||
t.Errorf("Values of omitted empty fields should be not counted")
|
||||
}
|
||||
|
||||
if s[0].(string) != "example" {
|
||||
t.Errorf("Values of omitted empty fields should left the value example")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues_OmitNested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
a := A{
|
||||
Name: "example",
|
||||
Value: 123,
|
||||
}
|
||||
|
||||
type B struct {
|
||||
A A `structs:",omitnested"`
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
s := Values(b)
|
||||
|
||||
if len(s) != 2 {
|
||||
t.Errorf("Values of omitted nested struct should be not counted")
|
||||
}
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{123, a} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Values should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues_Nested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
s := Values(b)
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"example", 123} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Values should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues_Anonymous(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A
|
||||
C int
|
||||
}
|
||||
b := &B{C: 123}
|
||||
b.A = a
|
||||
|
||||
s := Values(b)
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"example", 123} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Values should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNames(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
|
||||
s := Names(T)
|
||||
|
||||
if len(s) != 3 {
|
||||
t.Errorf("Names should return a slice of len 3, got: %d", len(s))
|
||||
}
|
||||
|
||||
inSlice := func(val string) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []string{"A", "B", "C"} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Names should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFields(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
C: true,
|
||||
}
|
||||
|
||||
s := Fields(T)
|
||||
|
||||
if len(s) != 3 {
|
||||
t.Errorf("Fields should return a slice of len 3, got: %d", len(s))
|
||||
}
|
||||
|
||||
inSlice := func(val string) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v.Name(), val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []string{"A", "B", "C"} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Fields should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFields_OmitNested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A
|
||||
C int
|
||||
Value string `structs:"-"`
|
||||
Number int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
s := Fields(b)
|
||||
|
||||
if len(s) != 3 {
|
||||
t.Errorf("Fields should omit nested struct. Expecting 2 got: %d", len(s))
|
||||
}
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v.Name(), val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"A", "C"} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Fields should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFields_Anonymous(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A
|
||||
C int
|
||||
}
|
||||
b := &B{C: 123}
|
||||
b.A = a
|
||||
|
||||
s := Fields(b)
|
||||
|
||||
inSlice := func(val interface{}) bool {
|
||||
for _, v := range s {
|
||||
if reflect.DeepEqual(v.Name(), val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, val := range []interface{}{"A", "C"} {
|
||||
if !inSlice(val) {
|
||||
t.Errorf("Fields should have the value %v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool `structs:"-"`
|
||||
D []string
|
||||
}{}
|
||||
|
||||
ok := IsZero(T)
|
||||
if !ok {
|
||||
t.Error("IsZero should return true because none of the fields are initialized.")
|
||||
}
|
||||
|
||||
var X = struct {
|
||||
A string
|
||||
F *bool
|
||||
}{
|
||||
A: "a-value",
|
||||
}
|
||||
|
||||
ok = IsZero(X)
|
||||
if ok {
|
||||
t.Error("IsZero should return false because A is initialized")
|
||||
}
|
||||
|
||||
var Y = struct {
|
||||
A string
|
||||
B int
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 123,
|
||||
}
|
||||
|
||||
ok = IsZero(Y)
|
||||
if ok {
|
||||
t.Error("IsZero should return false because A and B is initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZero_OmitNested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A `structs:",omitnested"`
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
ok := IsZero(b)
|
||||
if ok {
|
||||
t.Error("IsZero should return false because A, B and C are initialized")
|
||||
}
|
||||
|
||||
aZero := A{}
|
||||
bZero := &B{A: aZero}
|
||||
|
||||
ok = IsZero(bZero)
|
||||
if !ok {
|
||||
t.Error("IsZero should return true because neither A nor B is initialized")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsZero_Nested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
ok := IsZero(b)
|
||||
if ok {
|
||||
t.Error("IsZero should return false because A, B and C are initialized")
|
||||
}
|
||||
|
||||
aZero := A{}
|
||||
bZero := &B{A: aZero}
|
||||
|
||||
ok = IsZero(bZero)
|
||||
if !ok {
|
||||
t.Error("IsZero should return true because neither A nor B is initialized")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsZero_Anonymous(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A
|
||||
C int
|
||||
}
|
||||
b := &B{C: 123}
|
||||
b.A = a
|
||||
|
||||
ok := IsZero(b)
|
||||
if ok {
|
||||
t.Error("IsZero should return false because A, B and C are initialized")
|
||||
}
|
||||
|
||||
aZero := A{}
|
||||
bZero := &B{}
|
||||
bZero.A = aZero
|
||||
|
||||
ok = IsZero(bZero)
|
||||
if !ok {
|
||||
t.Error("IsZero should return true because neither A nor B is initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasZero(t *testing.T) {
|
||||
var T = struct {
|
||||
A string
|
||||
B int
|
||||
C bool `structs:"-"`
|
||||
D []string
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 2,
|
||||
}
|
||||
|
||||
ok := HasZero(T)
|
||||
if !ok {
|
||||
t.Error("HasZero should return true because A and B are initialized.")
|
||||
}
|
||||
|
||||
var X = struct {
|
||||
A string
|
||||
F *bool
|
||||
}{
|
||||
A: "a-value",
|
||||
}
|
||||
|
||||
ok = HasZero(X)
|
||||
if !ok {
|
||||
t.Error("HasZero should return true because A is initialized")
|
||||
}
|
||||
|
||||
var Y = struct {
|
||||
A string
|
||||
B int
|
||||
}{
|
||||
A: "a-value",
|
||||
B: 123,
|
||||
}
|
||||
|
||||
ok = HasZero(Y)
|
||||
if ok {
|
||||
t.Error("HasZero should return false because A and B is initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasZero_OmitNested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A `structs:",omitnested"`
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
// Because the Field A inside B is omitted HasZero should return false
|
||||
// because it will stop iterating deeper andnot going to lookup for D
|
||||
ok := HasZero(b)
|
||||
if ok {
|
||||
t.Error("HasZero should return false because A and C are initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasZero_Nested(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A A
|
||||
C int
|
||||
}
|
||||
b := &B{A: a, C: 123}
|
||||
|
||||
ok := HasZero(b)
|
||||
if !ok {
|
||||
t.Error("HasZero should return true because D is not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasZero_Anonymous(t *testing.T) {
|
||||
type A struct {
|
||||
Name string
|
||||
D string
|
||||
}
|
||||
a := A{Name: "example"}
|
||||
|
||||
type B struct {
|
||||
A
|
||||
C int
|
||||
}
|
||||
b := &B{C: 123}
|
||||
b.A = a
|
||||
|
||||
ok := HasZero(b)
|
||||
if !ok {
|
||||
t.Error("HasZero should return false because D is not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
type Foo struct {
|
||||
A string
|
||||
B bool
|
||||
}
|
||||
f := &Foo{}
|
||||
|
||||
n := Name(f)
|
||||
if n != "Foo" {
|
||||
t.Errorf("Name should return Foo, got: %s", n)
|
||||
}
|
||||
|
||||
unnamed := struct{ Name string }{Name: "Cihangir"}
|
||||
m := Name(unnamed)
|
||||
if m != "" {
|
||||
t.Errorf("Name should return empty string for unnamed struct, got: %s", n)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
t.Error("Name should panic if a non struct is passed")
|
||||
}
|
||||
}()
|
||||
|
||||
Name([]string{})
|
||||
}
|
||||
|
||||
func TestNestedNilPointer(t *testing.T) {
|
||||
type Collar struct {
|
||||
Engraving string
|
||||
}
|
||||
|
||||
type Dog struct {
|
||||
Name string
|
||||
Collar *Collar
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Dog *Dog
|
||||
}
|
||||
|
||||
person := &Person{
|
||||
Name: "John",
|
||||
}
|
||||
|
||||
personWithDog := &Person{
|
||||
Name: "Ron",
|
||||
Dog: &Dog{
|
||||
Name: "Rover",
|
||||
},
|
||||
}
|
||||
|
||||
personWithDogWithCollar := &Person{
|
||||
Name: "Kon",
|
||||
Dog: &Dog{
|
||||
Name: "Ruffles",
|
||||
Collar: &Collar{
|
||||
Engraving: "If lost, call Kon",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
fmt.Printf("err %+v\n", err)
|
||||
t.Error("Internal nil pointer should not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_ = Map(person) // Panics
|
||||
_ = Map(personWithDog) // Panics
|
||||
_ = Map(personWithDogWithCollar) // Doesn't panic
|
||||
}
|
32
Godeps/_workspace/src/github.com/fatih/structs/tags.go
generated
vendored
Normal file
32
Godeps/_workspace/src/github.com/fatih/structs/tags.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
package structs
|
||||
|
||||
import "strings"
|
||||
|
||||
// tagOptions contains a slice of tag options
|
||||
type tagOptions []string
|
||||
|
||||
// Has returns true if the given optiton is available in tagOptions
|
||||
func (t tagOptions) Has(opt string) bool {
|
||||
for _, tagOpt := range t {
|
||||
if tagOpt == opt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// parseTag splits a struct field's tag into its name and a list of options
|
||||
// which comes after a name. A tag is in the form of: "name,option1,option2".
|
||||
// The name can be neglectected.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
// tag is one of followings:
|
||||
// ""
|
||||
// "name"
|
||||
// "name,opt"
|
||||
// "name,opt,opt2"
|
||||
// ",opt"
|
||||
|
||||
res := strings.Split(tag, ",")
|
||||
return res[0], res[1:]
|
||||
}
|
46
Godeps/_workspace/src/github.com/fatih/structs/tags_test.go
generated
vendored
Normal file
46
Godeps/_workspace/src/github.com/fatih/structs/tags_test.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
package structs
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseTag_Name(t *testing.T) {
|
||||
tags := []struct {
|
||||
tag string
|
||||
has bool
|
||||
}{
|
||||
{"", false},
|
||||
{"name", true},
|
||||
{"name,opt", true},
|
||||
{"name , opt, opt2", false}, // has a single whitespace
|
||||
{", opt, opt2", false},
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
name, _ := parseTag(tag.tag)
|
||||
|
||||
if (name != "name") && tag.has {
|
||||
t.Errorf("Parse tag should return name: %#v", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTag_Opts(t *testing.T) {
|
||||
tags := []struct {
|
||||
opts string
|
||||
has bool
|
||||
}{
|
||||
{"name", false},
|
||||
{"name,opt", true},
|
||||
{"name , opt, opt2", false}, // has a single whitespace
|
||||
{",opt, opt2", true},
|
||||
{", opt3, opt4", false},
|
||||
}
|
||||
|
||||
// search for "opt"
|
||||
for _, tag := range tags {
|
||||
_, opts := parseTag(tag.opts)
|
||||
|
||||
if opts.Has("opt") != tag.has {
|
||||
t.Errorf("Tag opts should have opt: %#v", tag)
|
||||
}
|
||||
}
|
||||
}
|
74
builtin/logical/pki/backend.go
Normal file
74
builtin/logical/pki/backend.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// Factory creates a new backend implementing the logical.Backend interface
|
||||
func Factory(map[string]string) (logical.Backend, error) {
|
||||
return Backend(), nil
|
||||
}
|
||||
|
||||
// Backend returns a new Backend framework struct
|
||||
func Backend() *framework.Backend {
|
||||
var b backend
|
||||
b.Backend = &framework.Backend{
|
||||
Help: strings.TrimSpace(backendHelp),
|
||||
|
||||
PathsSpecial: &logical.Paths{
|
||||
Root: []string{
|
||||
"config/*",
|
||||
"revoke/*",
|
||||
"crl/rotate",
|
||||
},
|
||||
Unauthenticated: []string{
|
||||
"cert/*",
|
||||
"ca/pem",
|
||||
"ca",
|
||||
"crl/pem",
|
||||
"crl",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: []*framework.Path{
|
||||
pathRoles(&b),
|
||||
pathConfigCA(&b),
|
||||
pathConfigCRL(&b),
|
||||
pathIssue(&b),
|
||||
pathRotateCRL(&b),
|
||||
pathFetchCA(&b),
|
||||
pathFetchCRL(&b),
|
||||
pathFetchCRLViaCertPath(&b),
|
||||
pathFetchValid(&b),
|
||||
pathRevoke(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
secretCerts(&b),
|
||||
},
|
||||
}
|
||||
|
||||
b.crlLifetime = time.Hour * 72
|
||||
b.revokeStorageLock = &sync.Mutex{}
|
||||
|
||||
return b.Backend
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
|
||||
crlLifetime time.Duration
|
||||
revokeStorageLock *sync.Mutex
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The PKI backend dynamically generates X509 server and client certificates.
|
||||
|
||||
After mounting this backend, configure the CA using the "pem_bundle" endpoint within
|
||||
the "config/" path.
|
||||
`
|
448
builtin/logical/pki/backend_test.go
Normal file
448
builtin/logical/pki/backend_test.go
Normal file
|
@ -0,0 +1,448 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var (
|
||||
stepCount = 0
|
||||
)
|
||||
|
||||
// Performs basic tests on CA functionality
|
||||
func TestBackend_basic(t *testing.T) {
|
||||
b := Backend()
|
||||
|
||||
testCase := logicaltest.TestCase{
|
||||
Backend: b,
|
||||
Steps: []logicaltest.TestStep{},
|
||||
}
|
||||
|
||||
stepCount += len(testCase.Steps)
|
||||
|
||||
testCase.Steps = append(testCase.Steps, generateCASteps(t)...)
|
||||
|
||||
logicaltest.Test(t, testCase)
|
||||
}
|
||||
|
||||
// Generates and tests steps that walk through the various possibilities
|
||||
// of role flags to ensure that they are properly restricted
|
||||
func TestBackend_roles(t *testing.T) {
|
||||
b := Backend()
|
||||
|
||||
testCase := logicaltest.TestCase{
|
||||
Backend: b,
|
||||
Steps: []logicaltest.TestStep{},
|
||||
}
|
||||
|
||||
testCase.Steps = append(testCase.Steps, generateCASteps(t)...)
|
||||
testCase.Steps = append(testCase.Steps, generateRoleSteps(t)...)
|
||||
if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 {
|
||||
for i, v := range testCase.Steps {
|
||||
fmt.Printf("Step %d:\n%+v\n\n", i+stepCount, v)
|
||||
}
|
||||
}
|
||||
|
||||
stepCount += len(testCase.Steps)
|
||||
|
||||
logicaltest.Test(t, testCase)
|
||||
}
|
||||
|
||||
// Performs some validity checking on the returned bundles
|
||||
func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) {
|
||||
parsedCertBundle, err := certBundle.ToParsedCertBundle()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing cert bundle: %s", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case parsedCertBundle.Certificate == nil:
|
||||
return nil, fmt.Errorf("Did not find a certificate in the cert bundle")
|
||||
case parsedCertBundle.IssuingCA == nil:
|
||||
return nil, fmt.Errorf("Did not find a CA in the cert bundle")
|
||||
case parsedCertBundle.PrivateKey == nil:
|
||||
return nil, fmt.Errorf("Did not find a private key in the cert bundle")
|
||||
case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey:
|
||||
return nil, fmt.Errorf("Could not figure out type of private key")
|
||||
}
|
||||
|
||||
switch {
|
||||
case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa":
|
||||
fallthrough
|
||||
case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec":
|
||||
return nil, fmt.Errorf("Given key type does not match type found in bundle")
|
||||
}
|
||||
|
||||
cert := parsedCertBundle.Certificate
|
||||
// There should only be one usage type, because only one is requested
|
||||
// in the tests
|
||||
if len(cert.ExtKeyUsage) != 1 {
|
||||
return nil, fmt.Errorf("Got wrong size key usage in generated cert")
|
||||
}
|
||||
switch usage {
|
||||
case serverUsage:
|
||||
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth {
|
||||
return nil, fmt.Errorf("Bad key usage")
|
||||
}
|
||||
case clientUsage:
|
||||
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth {
|
||||
return nil, fmt.Errorf("Bad key usage")
|
||||
}
|
||||
case codeSigningUsage:
|
||||
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning {
|
||||
return nil, fmt.Errorf("Bad key usage")
|
||||
}
|
||||
}
|
||||
|
||||
if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 {
|
||||
return nil, fmt.Errorf("Validity period starts out of range")
|
||||
}
|
||||
|
||||
if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 {
|
||||
return nil, fmt.Errorf("Validity period too large")
|
||||
}
|
||||
|
||||
return parsedCertBundle, nil
|
||||
}
|
||||
|
||||
// Generates steps to test out CA configuration -- certificates + CRL expiry,
|
||||
// and ensure that the certificates are readable after storing them
|
||||
func generateCASteps(t *testing.T) []logicaltest.TestStep {
|
||||
ret := []logicaltest.TestStep{
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "config/ca",
|
||||
Data: map[string]interface{}{
|
||||
"pem_bundle": caKey + caCert,
|
||||
},
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "config/crl",
|
||||
Data: map[string]interface{}{
|
||||
"expiry": "16h",
|
||||
},
|
||||
},
|
||||
|
||||
// Ensure we can fetch it back via unauthenticated means, in various formats
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "cert/ca",
|
||||
Unauthenticated: true,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["certificate"].(string) != caCert {
|
||||
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", resp.Data["certificate"].(string), caCert)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "ca/pem",
|
||||
Unauthenticated: true,
|
||||
Check: func(resp *logical.Response) error {
|
||||
rawBytes := resp.Data["http_raw_body"].([]byte)
|
||||
if string(rawBytes) != caCert {
|
||||
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(rawBytes), caCert)
|
||||
}
|
||||
if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
|
||||
return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "ca",
|
||||
Unauthenticated: true,
|
||||
Check: func(resp *logical.Response) error {
|
||||
rawBytes := resp.Data["http_raw_body"].([]byte)
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rawBytes,
|
||||
})
|
||||
if string(pemBytes) != caCert {
|
||||
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(pemBytes), caCert)
|
||||
}
|
||||
if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
|
||||
return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "config/crl",
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["expiry"].(string) != "16h" {
|
||||
return fmt.Errorf("CRL lifetimes do not match (got %s)", resp.Data["expiry"].(string))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Generates steps to test out various role permutations
|
||||
func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
|
||||
roleVals := roleEntry{
|
||||
LeaseMax: "12h",
|
||||
}
|
||||
issueVals := certutil.IssueData{}
|
||||
ret := []logicaltest.TestStep{}
|
||||
|
||||
roleTestStep := logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "roles/test",
|
||||
}
|
||||
issueTestStep := logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "issue/test",
|
||||
}
|
||||
|
||||
genericErrorOkCheck := func(resp *logical.Response) error {
|
||||
if resp.IsError() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Expected an error, but did not seem to get one")
|
||||
}
|
||||
|
||||
// Adds tests with the currently configured issue/role information
|
||||
addTests := func(testCheck logicaltest.TestCheckFunc) {
|
||||
//fmt.Printf("role vals: %#v\n", roleVals)
|
||||
//fmt.Printf("issue vals: %#v\n", issueTestStep)
|
||||
roleTestStep.Data = structs.New(roleVals).Map()
|
||||
ret = append(ret, roleTestStep)
|
||||
issueTestStep.Data = structs.New(issueVals).Map()
|
||||
switch {
|
||||
case issueTestStep.ErrorOk:
|
||||
issueTestStep.Check = genericErrorOkCheck
|
||||
case testCheck != nil:
|
||||
issueTestStep.Check = testCheck
|
||||
default:
|
||||
issueTestStep.Check = nil
|
||||
}
|
||||
ret = append(ret, issueTestStep)
|
||||
}
|
||||
|
||||
// Returns a TestCheckFunc that performs various validity checks on the
|
||||
// returned certificate information, mostly within checkCertsAndPrivateKey
|
||||
getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc {
|
||||
var certBundle certutil.CertBundle
|
||||
return func(resp *logical.Response) error {
|
||||
err := mapstructure.Decode(resp.Data, &certBundle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsedCertBundle, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error checking generated certificate: %s", err)
|
||||
}
|
||||
cert := parsedCertBundle.Certificate
|
||||
if cert.Subject.CommonName != name {
|
||||
return fmt.Errorf("Error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name)
|
||||
}
|
||||
if len(cert.DNSNames) != 1 {
|
||||
return fmt.Errorf("Error: found more than one DNS SAN but only one was requested")
|
||||
}
|
||||
if cert.DNSNames[0] != name {
|
||||
return fmt.Errorf("Error: returned certificate has a DNS SAN of %s but %s was requested", cert.DNSNames[0], name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Common names to test with the various role flags toggled
|
||||
var commonNames struct {
|
||||
Localhost bool `structs:"localhost"`
|
||||
BaseDomain bool `structs:"foo.example.com"`
|
||||
Wildcard bool `structs:"*.example.com"`
|
||||
Subdomain bool `structs:"foo.bar.example.com"`
|
||||
SubdomainWildcard bool `structs:"*.bar.example.com"`
|
||||
AnyHost bool `structs:"porkslap.beer"`
|
||||
}
|
||||
|
||||
// Adds a series of tests based on the current selection of
|
||||
// allowed common names; contains some (seeded) randomness
|
||||
//
|
||||
// This allows for a variety of common names to be tested in various
|
||||
// combinations with allowed toggles of the role
|
||||
addCnTests := func() {
|
||||
cnMap := structs.New(commonNames).Map()
|
||||
// For the number of tests being run, this is known to hit all
|
||||
// of the various values below
|
||||
mathRand := rand.New(rand.NewSource(1))
|
||||
for name, allowedInt := range cnMap {
|
||||
roleVals.KeyType = "rsa"
|
||||
roleVals.KeyBits = 2048
|
||||
if mathRand.Int()%2 == 1 {
|
||||
roleVals.KeyType = "ec"
|
||||
roleVals.KeyBits = 224
|
||||
}
|
||||
|
||||
roleVals.ServerFlag = false
|
||||
roleVals.ClientFlag = false
|
||||
roleVals.CodeSigningFlag = false
|
||||
var usage certUsage
|
||||
i := mathRand.Int()
|
||||
switch {
|
||||
case i%3 == 0:
|
||||
usage = serverUsage
|
||||
roleVals.ServerFlag = true
|
||||
case i%2 == 0:
|
||||
usage = clientUsage
|
||||
roleVals.ClientFlag = true
|
||||
default:
|
||||
usage = codeSigningUsage
|
||||
roleVals.CodeSigningFlag = true
|
||||
}
|
||||
|
||||
allowed := allowedInt.(bool)
|
||||
issueVals.CommonName = name
|
||||
if allowed {
|
||||
issueTestStep.ErrorOk = false
|
||||
} else {
|
||||
issueTestStep.ErrorOk = true
|
||||
}
|
||||
|
||||
validity, _ := time.ParseDuration(roleVals.LeaseMax)
|
||||
addTests(getCnCheck(name, roleVals.KeyType, usage, validity))
|
||||
}
|
||||
}
|
||||
|
||||
// Common Name tests
|
||||
{
|
||||
// common_name not provided
|
||||
issueVals.CommonName = ""
|
||||
issueTestStep.ErrorOk = true
|
||||
addTests(nil)
|
||||
|
||||
// Nothing is allowed
|
||||
addCnTests()
|
||||
|
||||
roleVals.AllowLocalhost = true
|
||||
commonNames.Localhost = true
|
||||
addCnTests()
|
||||
|
||||
roleVals.AllowedBaseDomain = "foobar.com"
|
||||
addCnTests()
|
||||
|
||||
roleVals.AllowedBaseDomain = "example.com"
|
||||
commonNames.BaseDomain = true
|
||||
commonNames.Wildcard = true
|
||||
addCnTests()
|
||||
|
||||
roleVals.AllowSubdomains = true
|
||||
commonNames.Subdomain = true
|
||||
commonNames.SubdomainWildcard = true
|
||||
addCnTests()
|
||||
|
||||
roleVals.AllowAnyName = true
|
||||
commonNames.AnyHost = true
|
||||
addCnTests()
|
||||
}
|
||||
|
||||
// IP SAN tests
|
||||
{
|
||||
issueVals.IPSANs = "127.0.0.1,::1"
|
||||
issueTestStep.ErrorOk = true
|
||||
addTests(nil)
|
||||
|
||||
roleVals.AllowIPSANs = true
|
||||
issueTestStep.ErrorOk = false
|
||||
addTests(nil)
|
||||
|
||||
issueVals.IPSANs = "foobar"
|
||||
issueTestStep.ErrorOk = true
|
||||
addTests(nil)
|
||||
|
||||
issueTestStep.ErrorOk = false
|
||||
issueVals.IPSANs = ""
|
||||
}
|
||||
|
||||
// Lease tests
|
||||
{
|
||||
roleTestStep.ErrorOk = true
|
||||
roleVals.Lease = ""
|
||||
roleVals.LeaseMax = ""
|
||||
addTests(nil)
|
||||
|
||||
roleVals.Lease = "12h"
|
||||
roleVals.LeaseMax = "6h"
|
||||
addTests(nil)
|
||||
|
||||
roleTestStep.ErrorOk = false
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const (
|
||||
caKey string = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bHe+YGjlfl
|
||||
34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wUL5Z7ZlQD
|
||||
R4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15ItuP1yVUTDX
|
||||
TYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q7xwZncoM
|
||||
bQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuofvF4PS7S
|
||||
FzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABAoIBAQCp6VIdFdZcDYPd
|
||||
WIVuvBJfINiJo6AtURa2yX8BJggdPkRRCjTcWUwwFq1+wHDuwwtgidGTW9oxZxeU
|
||||
Psh1wlvcXN2+28C7ikAar/WUvsAeed44EV+1kXwJzV/89XyBFDnuazadqzcgUL0h
|
||||
gP4JLR9bhULsRFRkvanmW6zFzZpcjBzi/UoFuWkFRRqZ0euM2Lpz8L75PFfW9s9M
|
||||
kNglZpcV6ZmvR9c1JkEMUs/mrB8ZgCd1uvmcVosQ+u7sE8Yk/xAurHXuNJQlGXx4
|
||||
azrLW0XY1CLO2Tm4l4MwPjmhH0WytXNjOSKycBCXVnBIfZsI128DsP5YyA/fW9qA
|
||||
BAqFSzABAoGBAPcBNk9sf3cnZ5w6qwlE2ysDwGIGR+I1fb09YjRI6vjwwdWZgGR0
|
||||
EE4UB1Pp+KIehXaTJHcEgvBBErM2NLS4qKzh25O30C2EwK6o//3jEAribuYutBhJ
|
||||
ihu1qKzqcPbKClG+34kjX6nmtux2wlYM05f5v3ALki5Is7W/RrfceBuBAoGBAN2s
|
||||
hdt4TcgIcZymPG2931qCBGF3E8AaA8bUl9TKaZHuFikOMFKA/KM5O5mznPGnQP2d
|
||||
kXYKXuqdYhVLwp32FTbIbozGZZ8XliO5oS7J3vIID+sLWQhrvyFO7d0lpSjv41HH
|
||||
yJ2DrykHRg8hxsbh2D4By7olBx6Q2m+B8lPzHmlhAoGACHUeKvIIG0haH9tSZ+rX
|
||||
pk1mlPSqGXDDcWtcpXWptgRoXqv23Xmr5UCCT7k/Li3lW/4FzZ117kwMG97LRzTb
|
||||
ca/6GMC+fBCDmHdo7ISN1BGUwoTu3bYG6JP7xo/wdkLMv6fNd6CicerYcJhQZynh
|
||||
RN7kUy3SP4t1u89k2H7QDgECgYBpU0bKr8+tQq3Qs3+02OmeFHbGZJDCztmKiIqX
|
||||
tZERoGFxIme9W8IuP8xczGW+wCx2FH7/6g+NRDhNTBDtgvYzcGpugvnX7JoO4W1/
|
||||
ULWYpFID6QFlqeRHjDwivndKCykkO1vL07zPLsCQAglzh+16ENpe2KcYU9Ul9EVS
|
||||
tAp4IQKBgQDrb/NpiVx7NI6PyTCm6ctuUAYm3ihAiQNV4Bmr0liPDp9PozbqkhcF
|
||||
udNtivO4LlRb/PJ+DK6afDyH8aJQdDqe3NpDvyrmKiMSYOY3iVFvan4tbIiofxdQ
|
||||
flwiZUzox814fzXbxheO9Cs6pXz7PUBVU4fN0Y/hXJCfRO4Ns9152A==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
caCert string = `-----BEGIN CERTIFICATE-----
|
||||
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1
|
||||
MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH
|
||||
e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU
|
||||
L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu
|
||||
P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q
|
||||
7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo
|
||||
fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G
|
||||
A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo
|
||||
5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0
|
||||
OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9
|
||||
VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk
|
||||
ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5
|
||||
loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI
|
||||
S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa
|
||||
G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
)
|
270
builtin/logical/pki/cert_util.go
Normal file
270
builtin/logical/pki/cert_util.go
Normal file
|
@ -0,0 +1,270 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
type certUsage int
|
||||
|
||||
const (
|
||||
serverUsage certUsage = 1 << iota
|
||||
clientUsage
|
||||
codeSigningUsage
|
||||
)
|
||||
|
||||
type certCreationBundle struct {
|
||||
SigningBundle *certutil.ParsedCertBundle
|
||||
CACert *x509.Certificate
|
||||
CommonNames []string
|
||||
IPSANs []net.IP
|
||||
KeyType string
|
||||
KeyBits int
|
||||
Lease time.Duration
|
||||
Usage certUsage
|
||||
}
|
||||
|
||||
// Fetches the CA info. Unlike other certificates, the CA info is stored
|
||||
// in the backend as a CertBundle, because we are storing its private key
|
||||
func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, error) {
|
||||
bundleEntry, err := req.Storage.Get("config/ca_bundle")
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local CA certificate/key: %s", err)}
|
||||
}
|
||||
if bundleEntry == nil {
|
||||
return nil, certutil.UserError{Err: fmt.Sprintf("Backend must be configured with a CA certificate/key")}
|
||||
}
|
||||
|
||||
var bundle certutil.CertBundle
|
||||
if err := bundleEntry.DecodeJSON(&bundle); err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to decode local CA certificate/key: %s", err)}
|
||||
}
|
||||
|
||||
parsedBundle, err := bundle.ToParsedCertBundle()
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: err.Error()}
|
||||
}
|
||||
|
||||
if parsedBundle.Certificate == nil {
|
||||
return nil, certutil.InternalError{Err: "Stored CA information not able to be parsed"}
|
||||
}
|
||||
|
||||
return parsedBundle, nil
|
||||
}
|
||||
|
||||
// Allows fetching certificates from the backend; it handles the slightly
|
||||
// separate pathing for CA, CRL, and revoked certificates.
|
||||
func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) {
|
||||
var path string
|
||||
|
||||
switch {
|
||||
case serial == "ca":
|
||||
path = "ca"
|
||||
case serial == "crl":
|
||||
path = "crl"
|
||||
case strings.HasPrefix(prefix, "revoked/"):
|
||||
path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
|
||||
default:
|
||||
path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
|
||||
}
|
||||
|
||||
certEntry, err := req.Storage.Get(path)
|
||||
if err != nil || certEntry == nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Certificate with serial number %s not found", serial)}
|
||||
}
|
||||
|
||||
if certEntry.Value == nil || len(certEntry.Value) == 0 {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Returned certificate bytes for serial %s were empty", serial)}
|
||||
}
|
||||
|
||||
return certEntry, nil
|
||||
}
|
||||
|
||||
// Given a set of requested names for a certificate, verifies that all of them
|
||||
// match the various toggles set in the role for controlling issuance.
|
||||
// If one does not pass, it is returned in the string argument.
|
||||
func validateCommonNames(req *logical.Request, commonNames []string, role *roleEntry) (string, error) {
|
||||
hostnameRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error compiling hostname regex: %s", err)
|
||||
}
|
||||
subdomainRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))*$`)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error compiling subdomain regex: %s", err)
|
||||
}
|
||||
for _, name := range commonNames {
|
||||
if role.AllowLocalhost && name == "localhost" {
|
||||
continue
|
||||
}
|
||||
|
||||
sanitizedName := name
|
||||
isWildcard := false
|
||||
if strings.HasPrefix(name, "*.") {
|
||||
sanitizedName = name[2:]
|
||||
isWildcard = true
|
||||
}
|
||||
if !hostnameRegex.MatchString(sanitizedName) {
|
||||
return name, nil
|
||||
}
|
||||
if role.AllowAnyName {
|
||||
continue
|
||||
}
|
||||
|
||||
if role.AllowTokenDisplayName {
|
||||
if name == req.DisplayName {
|
||||
continue
|
||||
}
|
||||
|
||||
if role.AllowSubdomains {
|
||||
if strings.HasSuffix(name, "."+req.DisplayName) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(role.AllowedBaseDomain) != 0 {
|
||||
if strings.HasSuffix(name, "."+role.AllowedBaseDomain) {
|
||||
if role.AllowSubdomains {
|
||||
continue
|
||||
}
|
||||
|
||||
if subdomainRegex.MatchString(strings.TrimSuffix(name, "."+role.AllowedBaseDomain)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isWildcard && role.AllowedBaseDomain == sanitizedName {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Performs the heavy lifting of creating a certificate. Returns
|
||||
// a fully-filled-in ParsedCertBundle.
|
||||
func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBundle, error) {
|
||||
var clientPrivKey crypto.Signer
|
||||
var err error
|
||||
result := &certutil.ParsedCertBundle{}
|
||||
|
||||
var serialNumber *big.Int
|
||||
serialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting random serial number")}
|
||||
}
|
||||
|
||||
switch creationInfo.KeyType {
|
||||
case "rsa":
|
||||
result.PrivateKeyType = certutil.RSAPrivateKey
|
||||
clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits)
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating RSA private key")}
|
||||
}
|
||||
result.PrivateKey = clientPrivKey
|
||||
result.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey))
|
||||
case "ec":
|
||||
result.PrivateKeyType = certutil.ECPrivateKey
|
||||
var curve elliptic.Curve
|
||||
switch creationInfo.KeyBits {
|
||||
case 224:
|
||||
curve = elliptic.P224()
|
||||
case 256:
|
||||
curve = elliptic.P256()
|
||||
case 384:
|
||||
curve = elliptic.P384()
|
||||
case 521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, certutil.UserError{Err: fmt.Sprintf("Unsupported bit length for EC key: %d", creationInfo.KeyBits)}
|
||||
}
|
||||
clientPrivKey, err = ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating EC private key")}
|
||||
}
|
||||
result.PrivateKey = clientPrivKey
|
||||
result.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Error marshalling EC private key")}
|
||||
}
|
||||
default:
|
||||
return nil, certutil.UserError{Err: fmt.Sprintf("Unknown key type: %s", creationInfo.KeyType)}
|
||||
}
|
||||
|
||||
subjKeyID, err := certutil.GetSubjKeyID(result.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting subject key ID: %s", err)}
|
||||
}
|
||||
|
||||
subject := pkix.Name{
|
||||
Country: creationInfo.CACert.Subject.Country,
|
||||
Organization: creationInfo.CACert.Subject.Organization,
|
||||
OrganizationalUnit: creationInfo.CACert.Subject.OrganizationalUnit,
|
||||
Locality: creationInfo.CACert.Subject.Locality,
|
||||
Province: creationInfo.CACert.Subject.Province,
|
||||
StreetAddress: creationInfo.CACert.Subject.StreetAddress,
|
||||
PostalCode: creationInfo.CACert.Subject.PostalCode,
|
||||
SerialNumber: serialNumber.String(),
|
||||
CommonName: creationInfo.CommonNames[0],
|
||||
}
|
||||
|
||||
certTemplate := &x509.Certificate{
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
SerialNumber: serialNumber,
|
||||
Subject: subject,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(creationInfo.Lease),
|
||||
KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement),
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
SubjectKeyId: subjKeyID,
|
||||
DNSNames: creationInfo.CommonNames,
|
||||
IPAddresses: creationInfo.IPSANs,
|
||||
PermittedDNSDomainsCritical: false,
|
||||
PermittedDNSDomains: nil,
|
||||
CRLDistributionPoints: creationInfo.CACert.CRLDistributionPoints,
|
||||
}
|
||||
|
||||
if creationInfo.Usage&serverUsage != 0 {
|
||||
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
|
||||
}
|
||||
if creationInfo.Usage&clientUsage != 0 {
|
||||
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
|
||||
}
|
||||
if creationInfo.Usage&codeSigningUsage != 0 {
|
||||
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning)
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to create certificate: %s", err)}
|
||||
}
|
||||
|
||||
result.CertificateBytes = cert
|
||||
result.Certificate, err = x509.ParseCertificate(cert)
|
||||
if err != nil {
|
||||
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to parse created certificate: %s", err)}
|
||||
}
|
||||
|
||||
result.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes
|
||||
result.IssuingCA = creationInfo.SigningBundle.Certificate
|
||||
|
||||
return result, nil
|
||||
}
|
195
builtin/logical/pki/crl_util.go
Normal file
195
builtin/logical/pki/crl_util.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
type revocationInfo struct {
|
||||
CertificateBytes []byte `json:"certificate_bytes"`
|
||||
RevocationTime int64 `json:"revocation_time"`
|
||||
}
|
||||
|
||||
// Revokes a cert, and tries to be smart about error recovery
|
||||
func revokeCert(b *backend, req *logical.Request, serial string) (*logical.Response, error) {
|
||||
alreadyRevoked := false
|
||||
var revInfo revocationInfo
|
||||
|
||||
certEntry, err := fetchCertBySerial(req, "revoked/", serial)
|
||||
// Don't check error because it's expected that it may fail here;
|
||||
// just check for existence
|
||||
if certEntry != nil {
|
||||
// Verify that it is also deleted from certs/
|
||||
// in case of partial failure from an earlier run.
|
||||
certEntry, _ = fetchCertBySerial(req, "certs/", serial)
|
||||
if certEntry == nil {
|
||||
// Everything seems sane, so don't rebuild the CRL
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Still exists in certs/; set the revocation info, below it will
|
||||
// be removed from certs/ and the CRL rotated
|
||||
alreadyRevoked = true
|
||||
|
||||
revEntry, err := req.Storage.Get("revoked/" + serial)
|
||||
if revEntry == nil || err != nil {
|
||||
return nil, fmt.Errorf("Error getting existing revocation info")
|
||||
}
|
||||
|
||||
err = revEntry.DecodeJSON(&revInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error decoding existing revocation info")
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyRevoked {
|
||||
certEntry, err = fetchCertBySerial(req, "certs/", serial)
|
||||
switch err.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
case certutil.InternalError:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certEntry.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing certificate")
|
||||
}
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("Got a nil certificate")
|
||||
}
|
||||
|
||||
if cert.NotAfter.Before(time.Now()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
revInfo.CertificateBytes = certEntry.Value
|
||||
revInfo.RevocationTime = time.Now().Unix()
|
||||
|
||||
certEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating revocation entry")
|
||||
}
|
||||
|
||||
err = req.Storage.Put(certEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error saving revoked certificate to new location")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
crlErr := buildCRL(b, req)
|
||||
switch crlErr.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
|
||||
case certutil.InternalError:
|
||||
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
|
||||
}
|
||||
|
||||
err = req.Storage.Delete("certs/" + serial)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error deleting cert from valid-certs location")
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"revocation_time": revInfo.RevocationTime,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Builds a CRL by going through the list of revoked certificates and building
|
||||
// a new CRL with the stored revocation times and serial numbers.
|
||||
//
|
||||
// If a certificate has already expired, it will be removed entirely rather than
|
||||
// become part of the new CRL.
|
||||
func buildCRL(b *backend, req *logical.Request) error {
|
||||
revokedSerials, err := req.Storage.List("revoked/")
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error fetching list of revoked certs: %s", err)}
|
||||
}
|
||||
|
||||
revokedCerts := []pkix.RevokedCertificate{}
|
||||
var revInfo revocationInfo
|
||||
for _, serial := range revokedSerials {
|
||||
revokedEntry, err := req.Storage.Get("revoked/" + serial)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Unable to fetch revoked cert with serial %s: %s", serial, err)}
|
||||
}
|
||||
if revokedEntry == nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Revoked certificate entry for serial %s is nil", serial)}
|
||||
}
|
||||
if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 {
|
||||
// TODO: In this case, remove it and continue? How likely is this to
|
||||
// happen? Alternately, could skip it entirely, or could implement a
|
||||
// delete function so that there is a way to remove these
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Found revoked serial but actual certificate is empty")}
|
||||
}
|
||||
|
||||
err = revokedEntry.DecodeJSON(&revInfo)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)}
|
||||
}
|
||||
|
||||
revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Unable to parse stored revoked certificate with serial %s: %s", serial, err)}
|
||||
}
|
||||
|
||||
if revokedCert.NotAfter.Before(time.Now()) {
|
||||
err = req.Storage.Delete(serial)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err)}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
revokedCerts = append(revokedCerts, pkix.RevokedCertificate{
|
||||
SerialNumber: revokedCert.SerialNumber,
|
||||
RevocationTime: time.Unix(revInfo.RevocationTime, 0),
|
||||
})
|
||||
}
|
||||
|
||||
signingBundle, caErr := fetchCAInfo(req)
|
||||
switch caErr.(type) {
|
||||
case certutil.UserError:
|
||||
return certutil.UserError{Err: fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)}
|
||||
case certutil.InternalError:
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CA certificate: %s", caErr)}
|
||||
}
|
||||
|
||||
crlLifetime := b.crlLifetime
|
||||
crlInfo, err := b.CRL(req.Storage)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CRL config information: %s", err)}
|
||||
}
|
||||
if crlInfo != nil {
|
||||
crlDur, err := time.ParseDuration(crlInfo.Expiry)
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error parsing CRL duration of %s", crlInfo.Expiry)}
|
||||
}
|
||||
crlLifetime = crlDur
|
||||
}
|
||||
|
||||
crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime))
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error creating new CRL: %s", err)}
|
||||
}
|
||||
|
||||
err = req.Storage.Put(&logical.StorageEntry{
|
||||
Key: "crl",
|
||||
Value: crlBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return certutil.InternalError{Err: fmt.Sprintf("Error storing CRL: %s", err)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
106
builtin/logical/pki/path_config_ca.go
Normal file
106
builtin/logical/pki/path_config_ca.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathConfigCA(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/ca",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"pem_bundle": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `PEM-format, concatenated unencrypted secret key
|
||||
and certificate`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathCAWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathConfigCAHelpSyn,
|
||||
HelpDescription: pathConfigCAHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathCAWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
pemBundle := d.Get("pem_bundle").(string)
|
||||
|
||||
parsedBundle, err := certutil.ParsePEMBundle(pemBundle)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case certutil.InternalError:
|
||||
return nil, err
|
||||
default:
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the case of a self-signed certificate
|
||||
if parsedBundle.Certificate == nil && parsedBundle.IssuingCA != nil {
|
||||
parsedBundle.Certificate = parsedBundle.IssuingCA
|
||||
parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes
|
||||
}
|
||||
|
||||
// TODO?: CRLs can only be generated with RSA keys right now, in the
|
||||
// Go standard library. The plubming is here to support non-RSA keys
|
||||
// if the library gets support
|
||||
|
||||
if parsedBundle.PrivateKeyType != certutil.RSAPrivateKey {
|
||||
return logical.ErrorResponse("Currently, only RSA keys are supported for the CA certificate"), nil
|
||||
}
|
||||
|
||||
if !parsedBundle.Certificate.IsCA {
|
||||
return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil
|
||||
}
|
||||
|
||||
cb, err := parsedBundle.ToCertBundle()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error converting raw values into cert bundle: %s", err)
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config/ca_bundle", cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = req.Storage.Put(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For ease of later use, also store just the certificate at a known
|
||||
// location, plus a blank CRL
|
||||
entry.Key = "ca"
|
||||
entry.Value = parsedBundle.CertificateBytes
|
||||
err = req.Storage.Put(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry.Key = "crl"
|
||||
entry.Value = []byte{}
|
||||
err = req.Storage.Put(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const pathConfigCAHelpSyn = `
|
||||
Configure the CA certificate and private key used for generated credentials.
|
||||
`
|
||||
|
||||
const pathConfigCAHelpDesc = `
|
||||
This configures the CA information used for credentials
|
||||
generated by this backend. This must be a PEM-format, concatenated
|
||||
unencrypted secret key and certificate.
|
||||
|
||||
For security reasons, you can only view the certificate when reading this endpoint.
|
||||
`
|
102
builtin/logical/pki/path_config_crl.go
Normal file
102
builtin/logical/pki/path_config_crl.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// CRLConfig holds basic CRL configuration information
|
||||
type crlConfig struct {
|
||||
Expiry string `json:"expiry" mapstructure:"expiry" structs:"expiry"`
|
||||
}
|
||||
|
||||
func pathConfigCRL(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/crl",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"expiry": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The amount of time the generated CRL should be
|
||||
valid; defaults to 72 hours`,
|
||||
Default: "72h",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathCRLRead,
|
||||
logical.WriteOperation: b.pathCRLWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathConfigCRLHelpSyn,
|
||||
HelpDescription: pathConfigCRLHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) CRL(s logical.Storage) (*crlConfig, error) {
|
||||
entry, err := s.Get("config/crl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result crlConfig
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathCRLRead(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := b.CRL(req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: structs.New(config).Map(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathCRLWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
expiry := d.Get("expiry").(string)
|
||||
|
||||
_, err := time.ParseDuration(expiry)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Given expiry could not be decoded: %s", err)), nil
|
||||
}
|
||||
|
||||
config := &crlConfig{
|
||||
Expiry: expiry,
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config/crl", config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = req.Storage.Put(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const pathConfigCRLHelpSyn = `
|
||||
Configure the CRL expiration.
|
||||
`
|
||||
|
||||
const pathConfigCRLHelpDesc = `
|
||||
This endpoint allows configuration of the CRL lifetime.
|
||||
`
|
177
builtin/logical/pki/path_fetch.go
Normal file
177
builtin/logical/pki/path_fetch.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// Returns the CA in raw format
|
||||
func pathFetchCA(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `ca(/pem)?`,
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathFetchRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathFetchHelpSyn,
|
||||
HelpDescription: pathFetchHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the CRL in raw format
|
||||
func pathFetchCRL(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `crl(/pem)?`,
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathFetchRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathFetchHelpSyn,
|
||||
HelpDescription: pathFetchHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path
|
||||
// also handles returning the CA cert in a non-raw format.
|
||||
func pathFetchValid(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `cert/(?P<serial>[0-9A-Fa-f-:]+)`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"serial": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `Certificate serial number, in colon- or
|
||||
hyphen-separated octal`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathFetchRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathFetchHelpSyn,
|
||||
HelpDescription: pathFetchHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// This returns the CRL in a non-raw format
|
||||
func pathFetchCRLViaCertPath(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `cert/crl`,
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathFetchRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathFetchHelpSyn,
|
||||
HelpDescription: pathFetchHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) {
|
||||
var serial string
|
||||
var pemType string
|
||||
var contentType string
|
||||
var certEntry *logical.StorageEntry
|
||||
var funcErr error
|
||||
var certificate []byte
|
||||
response = &logical.Response{
|
||||
Data: map[string]interface{}{},
|
||||
}
|
||||
|
||||
// Some of these need to return raw and some non-raw;
|
||||
// this is basically handled by setting contentType or not.
|
||||
// Errors don't cause an immediate exit, because the raw
|
||||
// paths still need to return raw output.
|
||||
|
||||
switch {
|
||||
case req.Path == "ca" || req.Path == "ca/pem":
|
||||
serial = "ca"
|
||||
contentType = "application/pkix-cert"
|
||||
if req.Path == "ca/pem" {
|
||||
pemType = "CERTIFICATE"
|
||||
}
|
||||
case req.Path == "crl" || req.Path == "crl/pem":
|
||||
serial = "crl"
|
||||
contentType = "application/pkix-crl"
|
||||
if req.Path == "crl/pem" {
|
||||
pemType = "X509 CRL"
|
||||
}
|
||||
case req.Path == "cert/crl":
|
||||
serial = "crl"
|
||||
pemType = "X509 CRL"
|
||||
default:
|
||||
serial = data.Get("serial").(string)
|
||||
pemType = "CERTIFICATE"
|
||||
}
|
||||
if len(serial) == 0 {
|
||||
response = logical.ErrorResponse("The serial number must be provided")
|
||||
goto reply
|
||||
}
|
||||
|
||||
_, funcErr = fetchCAInfo(req)
|
||||
switch funcErr.(type) {
|
||||
case certutil.UserError:
|
||||
response = logical.ErrorResponse(fmt.Sprintf("%s", funcErr))
|
||||
goto reply
|
||||
case certutil.InternalError:
|
||||
retErr = funcErr
|
||||
goto reply
|
||||
}
|
||||
|
||||
certEntry, funcErr = fetchCertBySerial(req, req.Path, serial)
|
||||
switch funcErr.(type) {
|
||||
case certutil.UserError:
|
||||
response = logical.ErrorResponse(funcErr.Error())
|
||||
goto reply
|
||||
case certutil.InternalError:
|
||||
retErr = funcErr
|
||||
goto reply
|
||||
}
|
||||
|
||||
certificate = certEntry.Value
|
||||
|
||||
if len(pemType) != 0 {
|
||||
block := pem.Block{
|
||||
Type: pemType,
|
||||
Bytes: certEntry.Value,
|
||||
}
|
||||
certificate = pem.EncodeToMemory(&block)
|
||||
}
|
||||
|
||||
reply:
|
||||
switch {
|
||||
case len(contentType) != 0:
|
||||
response = &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: contentType,
|
||||
logical.HTTPRawBody: certificate,
|
||||
}}
|
||||
if retErr != nil {
|
||||
b.Logger().Printf("Possible error, but cannot return in raw response: %s. Note that an empty CA probably means none was configured, and an empty CRL is quite possibly correct", retErr)
|
||||
}
|
||||
retErr = nil
|
||||
response.Data[logical.HTTPStatusCode] = 200
|
||||
case retErr != nil:
|
||||
response = nil
|
||||
default:
|
||||
response.Data["certificate"] = string(certificate)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const pathFetchHelpSyn = `
|
||||
Fetch a CA, CRL, or non-revoked certificate.
|
||||
`
|
||||
|
||||
const pathFetchHelpDesc = `
|
||||
This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched.
|
||||
|
||||
Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding.
|
||||
`
|
201
builtin/logical/pki/path_issue.go
Normal file
201
builtin/logical/pki/path_issue.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathIssue(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `issue/(?P<role>\w+)`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"role": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The desired role with configuration for this
|
||||
request`,
|
||||
},
|
||||
"common_name": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The requested common name; if you want more than
|
||||
one, specify the alternative names in the
|
||||
alt_names map`,
|
||||
},
|
||||
"alt_names": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The requested Subject Alternative Names, if any,
|
||||
in a comma-delimited list`,
|
||||
},
|
||||
"ip_sans": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The requested IP SANs, if any, in a
|
||||
common-delimited list`,
|
||||
},
|
||||
"lease": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The requested lease",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathIssueCert,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathIssueCertHelpSyn,
|
||||
HelpDescription: pathIssueCertHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathIssueCert(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := data.Get("role").(string)
|
||||
|
||||
// Get the common name(s)
|
||||
var commonNames []string
|
||||
cn := data.Get("common_name").(string)
|
||||
if len(cn) == 0 {
|
||||
return logical.ErrorResponse("The common_name field is required"), nil
|
||||
}
|
||||
commonNames = []string{cn}
|
||||
|
||||
cnAlt := data.Get("alt_names").(string)
|
||||
if len(cnAlt) != 0 {
|
||||
for _, v := range strings.Split(cnAlt, ",") {
|
||||
commonNames = append(commonNames, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the role
|
||||
role, err := b.getRole(req.Storage, roleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", roleName)), nil
|
||||
}
|
||||
|
||||
// Get any IP SANs
|
||||
ipSANs := []net.IP{}
|
||||
|
||||
ipAlt := data.Get("ip_sans").(string)
|
||||
if len(ipAlt) != 0 {
|
||||
if !role.AllowIPSANs {
|
||||
return logical.ErrorResponse(fmt.Sprintf("IP Subject Alternative Names are not allowed in this role, but was provided %s", ipAlt)), nil
|
||||
}
|
||||
for _, v := range strings.Split(ipAlt, ",") {
|
||||
parsedIP := net.ParseIP(v)
|
||||
if parsedIP == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("The value '%s' is not a valid IP address", v)), nil
|
||||
}
|
||||
ipSANs = append(ipSANs, parsedIP)
|
||||
}
|
||||
}
|
||||
|
||||
leaseField := data.Get("lease").(string)
|
||||
if len(leaseField) == 0 {
|
||||
leaseField = role.Lease
|
||||
}
|
||||
|
||||
lease, err := time.ParseDuration(leaseField)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Invalid requested lease: %s", err)), nil
|
||||
}
|
||||
leaseMax, err := time.ParseDuration(role.LeaseMax)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Invalid lease: %s", err)), nil
|
||||
}
|
||||
|
||||
if lease > leaseMax {
|
||||
return logical.ErrorResponse("Lease expires after maximum allowed by this role"), nil
|
||||
}
|
||||
|
||||
badName, err := validateCommonNames(req, commonNames, role)
|
||||
if len(badName) != 0 {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Name %s not allowed by this role", badName)), nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("Error validating name %s: %s", badName, err)
|
||||
}
|
||||
|
||||
signingBundle, caErr := fetchCAInfo(req)
|
||||
switch caErr.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)), nil
|
||||
case certutil.InternalError:
|
||||
return nil, fmt.Errorf("Error fetching CA certificate: %s", caErr)
|
||||
}
|
||||
|
||||
if time.Now().Add(lease).After(signingBundle.Certificate.NotAfter) {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil
|
||||
}
|
||||
|
||||
var usage certUsage
|
||||
if role.ServerFlag {
|
||||
usage = usage | serverUsage
|
||||
}
|
||||
if role.ClientFlag {
|
||||
usage = usage | clientUsage
|
||||
}
|
||||
if role.CodeSigningFlag {
|
||||
usage = usage | codeSigningUsage
|
||||
}
|
||||
|
||||
creationBundle := &certCreationBundle{
|
||||
SigningBundle: signingBundle,
|
||||
CACert: signingBundle.Certificate,
|
||||
CommonNames: commonNames,
|
||||
IPSANs: ipSANs,
|
||||
KeyType: role.KeyType,
|
||||
KeyBits: role.KeyBits,
|
||||
Lease: lease,
|
||||
Usage: usage,
|
||||
}
|
||||
|
||||
parsedBundle, err := createCertificate(creationBundle)
|
||||
switch err.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
case certutil.InternalError:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cb, err := parsedBundle.ToCertBundle()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err)
|
||||
}
|
||||
|
||||
resp := b.Secret(SecretCertsType).Response(
|
||||
structs.New(cb).Map(),
|
||||
map[string]interface{}{
|
||||
"serial_number": cb.SerialNumber,
|
||||
})
|
||||
|
||||
resp.Secret.Lease = lease
|
||||
|
||||
err = req.Storage.Put(&logical.StorageEntry{
|
||||
Key: "certs/" + cb.SerialNumber,
|
||||
Value: parsedBundle.CertificateBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to store certificate locally")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const pathIssueCertHelpSyn = `
|
||||
Request certificates using a certain role with the provided common name.
|
||||
`
|
||||
|
||||
const pathIssueCertHelpDesc = `
|
||||
This path allows requesting certificates to be issued according to the
|
||||
policy of the given role. The certificate will only be issued if the
|
||||
requested common name is allowed by the role policy.
|
||||
`
|
89
builtin/logical/pki/path_revoke.go
Normal file
89
builtin/logical/pki/path_revoke.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathRevoke(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `revoke`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"serial_number": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `Certificate serial number, in colon- or
|
||||
hyphen-separated octal`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathRevokeWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathRevokeHelpSyn,
|
||||
HelpDescription: pathRevokeHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func pathRotateCRL(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `crl/rotate`,
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathRotateCRLRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathRotateCRLHelpSyn,
|
||||
HelpDescription: pathRotateCRLHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
serial := data.Get("serial_number").(string)
|
||||
if len(serial) == 0 {
|
||||
return logical.ErrorResponse("The serial number must be provided"), nil
|
||||
}
|
||||
|
||||
b.revokeStorageLock.Lock()
|
||||
defer b.revokeStorageLock.Unlock()
|
||||
|
||||
return revokeCert(b, req, serial)
|
||||
}
|
||||
|
||||
func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
b.revokeStorageLock.Lock()
|
||||
defer b.revokeStorageLock.Unlock()
|
||||
|
||||
crlErr := buildCRL(b, req)
|
||||
switch crlErr.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
|
||||
case certutil.InternalError:
|
||||
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
|
||||
default:
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"success": true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
const pathRevokeHelpSyn = `
|
||||
Revoke a certificate by serial number.
|
||||
`
|
||||
|
||||
const pathRevokeHelpDesc = `
|
||||
This allows certificates to be revoked using its serial number. A root token is required.
|
||||
`
|
||||
|
||||
const pathRotateCRLHelpSyn = `
|
||||
Force a rebuild of the CRL.
|
||||
`
|
||||
|
||||
const pathRotateCRLHelpDesc = `
|
||||
Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required.
|
||||
`
|
276
builtin/logical/pki/path_roles.go
Normal file
276
builtin/logical/pki/path_roles.go
Normal file
|
@ -0,0 +1,276 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathRoles(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/(?P<name>\\w+)",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the role",
|
||||
},
|
||||
|
||||
"lease": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Default: "",
|
||||
Description: `The lease length if no specific lease length is
|
||||
requested. The lease length controls the expiration
|
||||
of certificates issued by this backend. Defaults to
|
||||
the value of lease_max.`,
|
||||
},
|
||||
|
||||
"lease_max": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Default: "",
|
||||
Description: "The maximum allowed lease length",
|
||||
},
|
||||
|
||||
"allow_localhost": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
Description: `Whether to allow "localhost" as a valid common
|
||||
name in a request`,
|
||||
},
|
||||
|
||||
"allowed_base_domain": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Default: "",
|
||||
Description: `If set, clients can request certificates for
|
||||
subdomains directly beneath this base domain, including
|
||||
the wildcard subdomain. See the documentation for more
|
||||
information.`,
|
||||
},
|
||||
|
||||
"allow_token_displayname": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: false,
|
||||
Description: `If set, clients can request certificates for
|
||||
matching the value of the Display Name on the requesting
|
||||
token. See the documentation for more information.`,
|
||||
},
|
||||
|
||||
"allow_subdomains": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: false,
|
||||
Description: `If set, clients can request certificates for
|
||||
subdomains of the CNs allowed by the other role options,
|
||||
including wildcard subdomains. See the documentation for
|
||||
more information.`,
|
||||
},
|
||||
|
||||
"allow_any_name": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: false,
|
||||
Description: `If set, clients can request certificates for
|
||||
any CN they like. See the documentation for more
|
||||
information.`,
|
||||
},
|
||||
|
||||
"allow_ip_sans": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
Description: `If set, IP Subject Alternative Names are allowed.
|
||||
Any valid IP is accepted.`,
|
||||
},
|
||||
|
||||
"server_flag": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
Description: `If set, certificates are flagged for server use.
|
||||
Defaults to true.`,
|
||||
},
|
||||
|
||||
"client_flag": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
Description: `If set, certificates are flagged for client use.
|
||||
Defaults to true.`,
|
||||
},
|
||||
|
||||
"code_signing_flag": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: false,
|
||||
Description: `If set, certificates are flagged for code signing
|
||||
use. Defaults to false.`,
|
||||
},
|
||||
|
||||
"key_type": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Default: "rsa",
|
||||
Description: `The type of key to use; defaults to RSA. "rsa"
|
||||
and "ec" are the only valid values.`,
|
||||
},
|
||||
|
||||
"key_bits": &framework.FieldSchema{
|
||||
Type: framework.TypeInt,
|
||||
Default: 2048,
|
||||
Description: `The number of bits to use. You will almost
|
||||
certainly want to change this if you adjust
|
||||
the key_type.`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathRoleRead,
|
||||
logical.WriteOperation: b.pathRoleCreate,
|
||||
logical.DeleteOperation: b.pathRoleDelete,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathRoleHelpSyn,
|
||||
HelpDescription: pathRoleHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) getRole(s logical.Storage, n string) (*roleEntry, error) {
|
||||
entry, err := s.Get("role/" + n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result roleEntry
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleDelete(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
err := req.Storage.Delete("role/" + data.Get("name").(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleRead(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
role, err := b.getRole(req.Storage, data.Get("name").(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: structs.New(role).Map(),
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleCreate(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
name := data.Get("name").(string)
|
||||
|
||||
entry := &roleEntry{
|
||||
LeaseMax: data.Get("lease_max").(string),
|
||||
Lease: data.Get("lease").(string),
|
||||
AllowLocalhost: data.Get("allow_localhost").(bool),
|
||||
AllowedBaseDomain: data.Get("allowed_base_domain").(string),
|
||||
AllowTokenDisplayName: data.Get("allow_token_displayname").(bool),
|
||||
AllowSubdomains: data.Get("allow_subdomains").(bool),
|
||||
AllowAnyName: data.Get("allow_any_name").(bool),
|
||||
AllowIPSANs: data.Get("allow_ip_sans").(bool),
|
||||
ServerFlag: data.Get("server_flag").(bool),
|
||||
ClientFlag: data.Get("client_flag").(bool),
|
||||
CodeSigningFlag: data.Get("code_signing_flag").(bool),
|
||||
KeyType: data.Get("key_type").(string),
|
||||
KeyBits: data.Get("key_bits").(int),
|
||||
}
|
||||
|
||||
if len(entry.LeaseMax) == 0 {
|
||||
return logical.ErrorResponse("\"lease_max\" value must be supplied"), nil
|
||||
}
|
||||
|
||||
leaseMax, err := time.ParseDuration(entry.LeaseMax)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Invalid lease: %s", err)), nil
|
||||
}
|
||||
|
||||
switch len(entry.Lease) {
|
||||
case 0:
|
||||
entry.Lease = entry.LeaseMax
|
||||
default:
|
||||
lease, err := time.ParseDuration(entry.Lease)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Invalid lease: %s", err)), nil
|
||||
}
|
||||
if lease > leaseMax {
|
||||
return logical.ErrorResponse("\"lease\" value must be less than \"lease_max\" value"), nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(entry.KeyType) == 0 {
|
||||
entry.KeyType = "rsa"
|
||||
}
|
||||
if entry.KeyBits == 0 {
|
||||
entry.KeyBits = 2048
|
||||
}
|
||||
|
||||
switch entry.KeyType {
|
||||
case "rsa":
|
||||
case "ec":
|
||||
switch entry.KeyBits {
|
||||
case 224:
|
||||
case 256:
|
||||
case 384:
|
||||
case 521:
|
||||
default:
|
||||
return logical.ErrorResponse(fmt.Sprintf("Unsupported bit length for EC key: %d", entry.KeyBits)), nil
|
||||
}
|
||||
default:
|
||||
return logical.ErrorResponse(fmt.Sprintf("Unknown key type %s", entry.KeyType)), nil
|
||||
}
|
||||
|
||||
// Store it
|
||||
jsonEntry, err := logical.StorageEntryJSON("role/"+name, entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Storage.Put(jsonEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type roleEntry struct {
|
||||
LeaseMax string `json:"lease_max" structs:"lease_max" mapstructure:"lease_max"`
|
||||
Lease string `json:"lease" structs:"lease" mapstructure:"lease"`
|
||||
AllowLocalhost bool `json:"allow_localhost" structs:"allow_localhost" mapstructure:"allow_localhost"`
|
||||
AllowedBaseDomain string `json:"allowed_base_domain" structs:"allowed_base_domain" mapstructure:"allowed_base_domain"`
|
||||
AllowTokenDisplayName bool `json:"allow_token_displayname" structs:"allow_token_displayname" mapstructure:"allow_token_displayname"`
|
||||
AllowSubdomains bool `json:"allow_subdomains" structs:"allow_subdomains" mapstructure:"allow_subdomains"`
|
||||
AllowAnyName bool `json:"allow_any_name" structs:"allow_any_name" mapstructure:"allow_any_name"`
|
||||
AllowIPSANs bool `json:"allow_ip_sans" structs:"allow_ip_sans" mapstructure:"allow_ip_sans"`
|
||||
ServerFlag bool `json:"server_flag" structs:"server_flag" mapstructure:"server_flag"`
|
||||
ClientFlag bool `json:"client_flag" structs:"client_flag" mapstructure:"client_flag"`
|
||||
CodeSigningFlag bool `json:"code_signing_flag" structs:"code_signing_flag" mapstructure:"code_signing_flag"`
|
||||
KeyType string `json:"key_type" structs:"key_type" mapstructure:"key_type"`
|
||||
KeyBits int `json:"key_bits" structs:"key_bits" mapstructure:"key_bits"`
|
||||
}
|
||||
|
||||
const pathRoleHelpSyn = `
|
||||
Manage the roles that can be created with this backend.
|
||||
`
|
||||
|
||||
const pathRoleHelpDesc = `
|
||||
This path lets you manage the roles that can be created with this backend.
|
||||
`
|
59
builtin/logical/pki/secret_certs.go
Normal file
59
builtin/logical/pki/secret_certs.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// SecretCertsType is the name used to identify this type
|
||||
const SecretCertsType = "pki"
|
||||
|
||||
func secretCerts(b *backend) *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: SecretCertsType,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"certificate": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The PEM-encoded concatenated certificate and
|
||||
issuing certificate authority`,
|
||||
},
|
||||
"private_key": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The PEM-encoded private key for the certificate",
|
||||
},
|
||||
"serial": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `The serial number of the certificate, for handy
|
||||
reference`,
|
||||
},
|
||||
},
|
||||
|
||||
DefaultDuration: 168 * time.Hour,
|
||||
DefaultGracePeriod: 10 * time.Minute,
|
||||
|
||||
Revoke: b.secretCredsRevoke,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) secretCredsRevoke(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
if req.Secret == nil {
|
||||
return nil, fmt.Errorf("Secret is nil in request")
|
||||
}
|
||||
|
||||
serialInt, ok := req.Secret.InternalData["serial_number"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Could not find serial in internal secret data")
|
||||
}
|
||||
|
||||
serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1)
|
||||
|
||||
b.revokeStorageLock.Lock()
|
||||
defer b.revokeStorageLock.Unlock()
|
||||
|
||||
return revokeCert(b, req, serial)
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/hashicorp/vault/builtin/logical/aws"
|
||||
"github.com/hashicorp/vault/builtin/logical/consul"
|
||||
"github.com/hashicorp/vault/builtin/logical/mysql"
|
||||
"github.com/hashicorp/vault/builtin/logical/pki"
|
||||
"github.com/hashicorp/vault/builtin/logical/postgresql"
|
||||
"github.com/hashicorp/vault/builtin/logical/ssh"
|
||||
"github.com/hashicorp/vault/builtin/logical/transit"
|
||||
|
@ -68,6 +69,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
|||
"aws": aws.Factory,
|
||||
"consul": consul.Factory,
|
||||
"postgresql": postgresql.Factory,
|
||||
"pki": pki.Factory,
|
||||
"transit": transit.Factory,
|
||||
"mysql": mysql.Factory,
|
||||
"ssh": ssh.Factory,
|
||||
|
|
344
helper/certutil/certutil_test.go
Normal file
344
helper/certutil/certutil_test.go
Normal file
|
@ -0,0 +1,344 @@
|
|||
package certutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
// Tests converting back and forth between a CertBundle and a ParsedCertBundle.
|
||||
//
|
||||
// Also tests the GetSubjKeyID, GetOctalFormatted, and
|
||||
// ParsedCertBundle.getSigner functions.
|
||||
func TestCertBundleConversion(t *testing.T) {
|
||||
cbuts := []*CertBundle{
|
||||
refreshRSACertBundle(),
|
||||
refreshECCertBundle(),
|
||||
}
|
||||
|
||||
for _, cbut := range cbuts {
|
||||
pcbut, err := cbut.ToParsedCertBundle()
|
||||
if err != nil {
|
||||
t.Fatalf("Error converting to parsed cert bundle: %s", err)
|
||||
}
|
||||
|
||||
err = compareCertBundleToParsedCertBundle(cbut, pcbut)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConfig(t *testing.T) {
|
||||
cbut := refreshRSACertBundle()
|
||||
|
||||
pcbut, err := cbut.ToParsedCertBundle()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting parsed cert bundle: %s", err)
|
||||
}
|
||||
|
||||
usages := []TLSUsage{
|
||||
TLSUnknown,
|
||||
TLSClient,
|
||||
TLSServer,
|
||||
TLSClient | TLSServer,
|
||||
}
|
||||
|
||||
for _, usage := range usages {
|
||||
tlsConfig, err := pcbut.GetTLSConfig(usage)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting tls config: %s", err)
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
t.Fatalf("Got nil tls.Config")
|
||||
}
|
||||
|
||||
if len(tlsConfig.Certificates) != 1 {
|
||||
t.Fatalf("Unexpected length in config.Certificates")
|
||||
}
|
||||
|
||||
// Length should be 2, since we passed in a CA
|
||||
if len(tlsConfig.Certificates[0].Certificate) != 2 {
|
||||
t.Fatalf("Did not find both certificates in config.Certificates.Certificate")
|
||||
}
|
||||
|
||||
if tlsConfig.Certificates[0].Leaf != pcbut.Certificate {
|
||||
t.Fatalf("Leaf certificate does not match parsed bundle's certificate")
|
||||
}
|
||||
|
||||
if tlsConfig.Certificates[0].PrivateKey != pcbut.PrivateKey {
|
||||
t.Fatalf("Config's private key does not match parsed bundle's private key")
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case TLSServer | TLSClient:
|
||||
if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
|
||||
t.Fatalf("CA certificate not in client cert pool as expected")
|
||||
}
|
||||
if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
|
||||
t.Fatalf("CA certificate not in root cert pool as expected")
|
||||
}
|
||||
case TLSServer:
|
||||
if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
|
||||
t.Fatalf("CA certificate not in client cert pool as expected")
|
||||
}
|
||||
if tlsConfig.RootCAs != nil {
|
||||
t.Fatalf("Found root pools in config object when not expected")
|
||||
}
|
||||
case TLSClient:
|
||||
if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
|
||||
t.Fatalf("CA certificate not in root cert pool as expected")
|
||||
}
|
||||
if tlsConfig.ClientCAs != nil {
|
||||
t.Fatalf("Found root pools in config object when not expected")
|
||||
}
|
||||
default:
|
||||
if tlsConfig.RootCAs != nil || tlsConfig.ClientCAs != nil {
|
||||
t.Fatalf("Found root pools in config object when not expected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertBundleParsing(t *testing.T) {
|
||||
jsonBundle := refreshRSACertBundle()
|
||||
jsonString, err := json.Marshal(jsonBundle)
|
||||
if err != nil {
|
||||
t.Fatalf("Error marshaling testing certbundle to JSON: %s", err)
|
||||
}
|
||||
pcbut, err := ParsePKIJSON(jsonString)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during JSON bundle handling: %s", err)
|
||||
}
|
||||
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
secret := &api.Secret{
|
||||
Data: structs.New(jsonBundle).Map(),
|
||||
}
|
||||
pcbut, err = ParsePKIMap(secret.Data)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during JSON bundle handling: %s", err)
|
||||
}
|
||||
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
pemBundle := strings.Join([]string{
|
||||
jsonBundle.Certificate,
|
||||
jsonBundle.IssuingCA,
|
||||
jsonBundle.PrivateKey,
|
||||
}, "\n")
|
||||
pcbut, err = ParsePEMBundle(pemBundle)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during JSON bundle handling: %s", err)
|
||||
}
|
||||
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func compareCertBundleToParsedCertBundle(cbut *CertBundle, pcbut *ParsedCertBundle) error {
|
||||
if cbut == nil {
|
||||
return fmt.Errorf("Got nil bundle")
|
||||
}
|
||||
if pcbut == nil {
|
||||
return fmt.Errorf("Got nil parsed bundle")
|
||||
}
|
||||
|
||||
switch {
|
||||
case pcbut.Certificate == nil:
|
||||
return fmt.Errorf("Parsed bundle has nil certificate")
|
||||
case pcbut.PrivateKey == nil:
|
||||
return fmt.Errorf("Parsed bundle has nil private key")
|
||||
case pcbut.IssuingCA == nil:
|
||||
return fmt.Errorf("Parsed bundle has nil issuing CA")
|
||||
}
|
||||
|
||||
switch cbut.PrivateKey {
|
||||
case privRSAKeyPem:
|
||||
if pcbut.PrivateKeyType != RSAPrivateKey {
|
||||
return fmt.Errorf("Parsed bundle has wrong private key type")
|
||||
}
|
||||
case privECKeyPem:
|
||||
if pcbut.PrivateKeyType != ECPrivateKey {
|
||||
return fmt.Errorf("Parsed bundle has wrong private key type")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Parsed bundle has unknown private key type")
|
||||
}
|
||||
|
||||
subjKeyID, err := GetSubjKeyID(pcbut.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when getting subject key id: %s", err)
|
||||
}
|
||||
if bytes.Compare(subjKeyID, pcbut.Certificate.SubjectKeyId) != 0 {
|
||||
return fmt.Errorf("Parsed bundle private key does not match subject key id")
|
||||
}
|
||||
|
||||
cb, err := pcbut.ToCertBundle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Thrown error during parsed bundle conversion: %s\n\nInput was: %#v", err, *pcbut)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(cb.Certificate) == 0:
|
||||
return fmt.Errorf("Bundle has nil certificate")
|
||||
case len(cb.PrivateKey) == 0:
|
||||
return fmt.Errorf("Bundle has nil private key")
|
||||
case len(cb.IssuingCA) == 0:
|
||||
return fmt.Errorf("Bundle has nil issuing CA")
|
||||
}
|
||||
|
||||
switch cb.PrivateKeyType {
|
||||
case "rsa":
|
||||
if pcbut.PrivateKeyType != RSAPrivateKey {
|
||||
return fmt.Errorf("Bundle has wrong private key type")
|
||||
}
|
||||
if cb.PrivateKey != privRSAKeyPem {
|
||||
return fmt.Errorf("Bundle private key does not match")
|
||||
}
|
||||
case "ec":
|
||||
if pcbut.PrivateKeyType != ECPrivateKey {
|
||||
return fmt.Errorf("Bundle has wrong private key type")
|
||||
}
|
||||
if cb.PrivateKey != privECKeyPem {
|
||||
return fmt.Errorf("Bundle private key does not match")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Bundle has unknown private key type")
|
||||
}
|
||||
|
||||
if cb.SerialNumber != GetOctalFormatted(pcbut.Certificate.SerialNumber.Bytes(), ":") {
|
||||
return fmt.Errorf("Bundle serial number does not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func refreshRSACertBundle() *CertBundle {
|
||||
return &CertBundle{
|
||||
Certificate: certRSAPem,
|
||||
PrivateKey: privRSAKeyPem,
|
||||
IssuingCA: issuingCaPem,
|
||||
}
|
||||
}
|
||||
|
||||
func refreshECCertBundle() *CertBundle {
|
||||
return &CertBundle{
|
||||
Certificate: certECPem,
|
||||
PrivateKey: privECKeyPem,
|
||||
IssuingCA: issuingCaPem,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
privRSAKeyPem = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAt3ZJUaztCRiVg87P0y8T7QMNFQi61BCSIKepxXXWc7zi5JJS
|
||||
MfQAstXJEqBYiShsSpYm6soiT6hX074t7wQAHGS3+u7qNogWpmTAUTUnNIM+QCxH
|
||||
2Nc/kzYxaWajupVzgGvLeiqU3d4tIUk/ZkftvWJryr2hZc8zEN3C4pGS/2F+RQ+z
|
||||
Ov+BpAI1BdbQGhF7m92vn6KS/iWsqmwHG9oChgvWeBHjWUI8qGauBc+it4S5RxfN
|
||||
8JJIBXUIZtbaqFZzgjv8kUDyqoQGvkY/4Ce1K0bFJsM7wmMPv+5QscIBF4KWgN0k
|
||||
TSMPPXfnn/QIAfhaYQkT9MjwGr6+B3SODNGCTQIDAQABAoIBAHtSwhprGbNhmS/P
|
||||
F5ioLsbFpEedZKkksnXM/qxDd/K45/Qp/6KgmM+eMdmZe6pHR/QjVunBEqtlSBSH
|
||||
5KykjcaIVbwSWdJqTH9xfm2YQ1BjYLcWjP1QQ+YbKb/mRO0phUiwLUlj0koKDWAw
|
||||
srN4anFB9Z+FNTcQvwz5ZQWUQbH0neQtWO1nDvLsScgu1kchoEzJEJaFOQ1+HfGe
|
||||
WxD766fZyqZQi5+cLrhOqHOGSlO+IFVe0hguiEHFr9LEPTXXkZtOR4wTf7j1Us8s
|
||||
1KQ/jv01sx9S7HEbZJurzIjS23OywEUdJd1EsIE2lJV2QUwSiAsPYZOSQZlgOGzP
|
||||
VRKVkGkCgYEA1u+pVP2r+xSxYy8KcdcRCdGGBh00VLx1yJRHWZ5YjF56hp0R0cG+
|
||||
xGLar5KCdBpr4jJnQGIrx8lw3SDCt4EXlxgJxitXlBtiKByM7/mYRRfURr9WMRr4
|
||||
88GQlWDbo2Xalnuac0qlkFqVIg0BaW+Z15A/E1L69aUxaR0ozlA9Jl8CgYEA2oNA
|
||||
5F2otqzo9eNYucNAjihVhATd11DECQvbIQp/0bEJe0Znnzq/QIGIOVapC0VKGBwB
|
||||
P5DuLL1P/nTPjjE/ZhjFuhMNM5PzC6obAjBh+gCpc+c+21Qerv7RKUTi2sGTzRHu
|
||||
lpccRDfuF8bhzD6lAo50FpSmPE/ovZzb9+IsXtMCgYBVnUdM9HKh47846870Q5+k
|
||||
0pHZM57ZtewQxoeZOgq5dxTFNCGZ9NvBLENBtlFCYBfjFQKt0azwutu7KUaGg+Ra
|
||||
qheSmUccVsAFjEHTgQ9XTkOfHq39h2ns5ohqCBfVAUhNstR14iEK3BoVYyrRzcNw
|
||||
6yNE1kPivzdsUFIlxC5nbwKBgDUUjT7sQX+eoTiZ8YOumo/t3Fglln4ncHeCGcj8
|
||||
8+/MQbFgeOuFKdBRpvXGx2mle0pAA02dtz3G/xeg6IpyDCSQ//cjiaFt3yyGNeli
|
||||
N2qznnY5RluhI5L+83BC+5iITY8TPBH4wzUPIRdFiLREw3DLigeyNG+SOcdVw1mD
|
||||
56NhAoGBALFh3sGkhvPiI/G/i/5tGZVA/dS/4DVXOoHW43+ZDHWEwqiN6vTf/VVi
|
||||
cm+8kcfLY1E5fSf/4e7mIQq7o5qVn9Y3HWsajS1FFeznJjPj4Jaa1HvegNcycAzs
|
||||
XOQ7xy23/8wUupgNeD1mFdSFCXQ3UedsJuVBHsElPc5W74q4F4+F
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
certRSAPem = `-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCAuSgAwIBAgIUcFCL9ESWTKLE6RqSYV7iZ78f1KcwCwYJKoZIhvcNAQEL
|
||||
MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyMzA0WhcN
|
||||
MTUwNzAzMTcyMzA0WjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDY0
|
||||
MTIwMzIxNzY3NTk2MjQyMjU0OTg5MTUxMzAyMjg1NzQ0NTc0OTkzMjY3NjI2MzCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALd2SVGs7QkYlYPOz9MvE+0D
|
||||
DRUIutQQkiCnqcV11nO84uSSUjH0ALLVyRKgWIkobEqWJurKIk+oV9O+Le8EABxk
|
||||
t/ru6jaIFqZkwFE1JzSDPkAsR9jXP5M2MWlmo7qVc4Bry3oqlN3eLSFJP2ZH7b1i
|
||||
a8q9oWXPMxDdwuKRkv9hfkUPszr/gaQCNQXW0BoRe5vdr5+ikv4lrKpsBxvaAoYL
|
||||
1ngR41lCPKhmrgXPoreEuUcXzfCSSAV1CGbW2qhWc4I7/JFA8qqEBr5GP+AntStG
|
||||
xSbDO8JjD7/uULHCAReCloDdJE0jDz1355/0CAH4WmEJE/TI8Bq+vgd0jgzRgk0C
|
||||
AwEAAaOCAQQwggEAMA4GA1UdDwEB/wQEAwIAqDAdBgNVHSUEFjAUBggrBgEFBQcD
|
||||
AQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUZHtkxSX5GVAYo3h8
|
||||
B8TGJ36vTH4wHwYDVR0jBBgwFoAU5JzeXhccaOWk5X6vhuGV7NwLMVkwTgYDVR0R
|
||||
BEcwRYIJbG9jYWxob3N0gg9mb28uZXhhbXBsZS5jb22CD2Jhci5leGFtcGxlLmNv
|
||||
bYcEgAMFBocQ/gEAAAAAAAAAAAAAAAAAATAxBgNVHR8EKjAoMCagJKAihiBodHRw
|
||||
Oi8vbG9jYWxob3N0OjgyMDAvdjEvcGtpL2NybDALBgkqhkiG9w0BAQsDggEBAAps
|
||||
W2ZDOAfwWufclmGPHt+YRXXSTWvPfF/cBeg5Oq/F8qUCVMHqdE/+EDWzh+Kz8jp0
|
||||
ggklnh76frROvHxygbVD2Hs9ACzgpnHPy8FYOdN+OblvAMtGlMyTq/5XheasmWdY
|
||||
FFH/ft6tReG7BjGgfdyH8yL/R6b/RtU/qPlowfrZgAzOv7/ou6yRlfjIhsWbne/S
|
||||
SQuGASRxRp3Txp7Cf3RcdCwVuiQhFLVeVHH+atTc8v2DO/CLfi9enQo96qUku8Bd
|
||||
b5QPKIV0sQdtwGV5fo2JGd25rWpCo6TkAM9EeNkcVze8wgArSRk8zLkvM/5z+5sn
|
||||
Qaka08px4wljGQ2Wc88=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
privECKeyPem = `-----BEGIN EC PRIVATE KEY-----
|
||||
MGgCAQEEHM3nuYLlrvawBN9hGVcu9mpaCEr7LMe44a7oQOygBwYFK4EEACGhPAM6
|
||||
AATBZ3VXwBE9oeSREpM5b25PW6WiuLb4EXWpKZyjj552QYKYe7QBuGe9wvvgOeCB
|
||||
ovN3tSuGKzTiUA==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
certECPem = `-----BEGIN CERTIFICATE-----
|
||||
MIIDJDCCAg6gAwIBAgIUM3J02tw0ZvpHUVHv6t8kcoft2/MwCwYJKoZIhvcNAQEL
|
||||
MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyODQyWhcN
|
||||
MTUwNzAzMTcyODQyWjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDI5
|
||||
MzcxMDk5Mzc2NDA3NDYyNjg3MTQzODcwMjc3Njg1OTkzMTkyMzkxNjM4MTE3MTBO
|
||||
MBAGByqGSM49AgEGBSuBBAAhAzoABMFndVfAET2h5JESkzlvbk9bpaK4tvgRdakp
|
||||
nKOPnnZBgph7tAG4Z73C++A54IGi83e1K4YrNOJQo4IBBDCCAQAwDgYDVR0PAQH/
|
||||
BAQDAgCoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E
|
||||
AjAAMB0GA1UdDgQWBBQiFoWDvInznUGjdJPjBAyoxIkQITAfBgNVHSMEGDAWgBTk
|
||||
nN5eFxxo5aTlfq+G4ZXs3AsxWTBOBgNVHREERzBFgglsb2NhbGhvc3SCD2Zvby5l
|
||||
eGFtcGxlLmNvbYIPYmFyLmV4YW1wbGUuY29thwSAAwUGhxD+AQAAAAAAAAAAAAAA
|
||||
AAABMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9sb2NhbGhvc3Q6ODIwMC92MS9w
|
||||
a2kvY3JsMAsGCSqGSIb3DQEBCwOCAQEA0RU18OdSdt2k4FKWyUS7EhVFOybiUHof
|
||||
1n9EeBoxd7fEP/IuQnJGr3CPV5LRFdHRxkihf4N5bRjsst7cqczaIZZLWkAj+P/2
|
||||
JxBqv2Hm57dwaw2gtwt3GcYN/5j76fYaoZOgPMqas72vYgnBgdKQs8GYSoy7BVpC
|
||||
x3nTYHwlOF+sM4wuVSi78lwkcgADF5GIWXrM3tYilmcT9fNbUgSvcVWdNTRJ0W+m
|
||||
S2AF+4eby5PC9U8eIoCnZPRNmH0jZbNWzZyD0hDhBrDlaEbS2QXKRURPHzht/SqN
|
||||
nWWcpQG3B8EI7p749dP5L+idi3ajHIH8vm/PK+o5TRrcHB585MlErQ==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
issuingCaPem = `-----BEGIN CERTIFICATE-----
|
||||
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1
|
||||
MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH
|
||||
e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU
|
||||
L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu
|
||||
P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q
|
||||
7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo
|
||||
fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G
|
||||
A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo
|
||||
5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0
|
||||
OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9
|
||||
VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk
|
||||
ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5
|
||||
loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI
|
||||
S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa
|
||||
G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==
|
||||
-----END CERTIFICATE-----`
|
||||
)
|
162
helper/certutil/helpers.go
Normal file
162
helper/certutil/helpers.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package certutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// GetOctalFormatted returns the byte buffer formatted in octal with
|
||||
// the specified separator between bytes.
|
||||
func GetOctalFormatted(buf []byte, sep string) string {
|
||||
var ret bytes.Buffer
|
||||
for _, cur := range buf {
|
||||
if ret.Len() > 0 {
|
||||
fmt.Fprintf(&ret, sep)
|
||||
}
|
||||
fmt.Fprintf(&ret, "%02x", cur)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
// GetSubjKeyID returns the subject key ID, e.g. the SHA1 sum
|
||||
// of the marshaled public key
|
||||
func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) {
|
||||
if privateKey == nil {
|
||||
return nil, InternalError{"Passed-in private key is nil"}
|
||||
}
|
||||
|
||||
marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
||||
if err != nil {
|
||||
return nil, InternalError{fmt.Sprintf("Error marshalling public key: %s", err)}
|
||||
}
|
||||
|
||||
subjKeyID := sha1.Sum(marshaledKey)
|
||||
|
||||
return subjKeyID[:], nil
|
||||
}
|
||||
|
||||
// ParsePKIMap takes a map (for instance, the Secret.Data
|
||||
// returned from the PKI backend) and returns a ParsedCertBundle.
|
||||
func ParsePKIMap(data map[string]interface{}) (*ParsedCertBundle, error) {
|
||||
result := &CertBundle{}
|
||||
err := mapstructure.Decode(data, result)
|
||||
if err != nil {
|
||||
return nil, UserError{err.Error()}
|
||||
}
|
||||
|
||||
return result.ToParsedCertBundle()
|
||||
}
|
||||
|
||||
// ParsePKIJSON takes a JSON-encoded string and returns a CertBundle
|
||||
// ParsedCertBundle.
|
||||
//
|
||||
// This can be either the output of an
|
||||
// issue call from the PKI backend or just its data member; or,
|
||||
// JSON not coming from the PKI backend.
|
||||
func ParsePKIJSON(input []byte) (*ParsedCertBundle, error) {
|
||||
result := &CertBundle{}
|
||||
err := json.Unmarshal(input, &result)
|
||||
|
||||
if err == nil {
|
||||
return result.ToParsedCertBundle()
|
||||
}
|
||||
|
||||
var secret Secret
|
||||
err = json.Unmarshal(input, &secret)
|
||||
|
||||
if err == nil {
|
||||
return ParsePKIMap(secret.Data)
|
||||
}
|
||||
|
||||
return nil, UserError{"Unable to parse out of either secret data or a secret object"}
|
||||
}
|
||||
|
||||
// ParsePEMBundle takes a string of concatenated PEM-format certificate
|
||||
// and private key values and decodes/parses them, checking validity along
|
||||
// the way. There must be at max two certificates (a certificate and its
|
||||
// issuing certificate) and one private key.
|
||||
func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) {
|
||||
if len(pemBundle) == 0 {
|
||||
return nil, UserError{"Empty PEM bundle"}
|
||||
}
|
||||
|
||||
pemBytes := []byte(pemBundle)
|
||||
var pemBlock *pem.Block
|
||||
parsedBundle := &ParsedCertBundle{}
|
||||
|
||||
for {
|
||||
pemBlock, pemBytes = pem.Decode(pemBytes)
|
||||
if pemBlock == nil {
|
||||
return nil, UserError{"No data found"}
|
||||
}
|
||||
|
||||
if signer, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
|
||||
if parsedBundle.PrivateKeyType != UnknownPrivateKey {
|
||||
return nil, UserError{"More than one private key given; provide only one private key in the bundle"}
|
||||
}
|
||||
parsedBundle.PrivateKeyType = ECPrivateKey
|
||||
parsedBundle.PrivateKeyBytes = pemBlock.Bytes
|
||||
parsedBundle.PrivateKey = signer
|
||||
|
||||
} else if signer, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
|
||||
if parsedBundle.PrivateKeyType != UnknownPrivateKey {
|
||||
return nil, UserError{"More than one private key given; provide only one private key in the bundle"}
|
||||
}
|
||||
parsedBundle.PrivateKeyType = RSAPrivateKey
|
||||
parsedBundle.PrivateKeyBytes = pemBlock.Bytes
|
||||
parsedBundle.PrivateKey = signer
|
||||
|
||||
} else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil {
|
||||
switch len(certificates) {
|
||||
case 0:
|
||||
return nil, UserError{"PEM block cannot be decoded to a private key or certificate"}
|
||||
|
||||
case 1:
|
||||
if parsedBundle.Certificate != nil {
|
||||
switch {
|
||||
// We just found the issuing CA
|
||||
case bytes.Equal(parsedBundle.Certificate.AuthorityKeyId, certificates[0].SubjectKeyId) && certificates[0].IsCA:
|
||||
parsedBundle.IssuingCABytes = pemBlock.Bytes
|
||||
parsedBundle.IssuingCA = certificates[0]
|
||||
|
||||
// Our saved certificate is actually the issuing CA
|
||||
case bytes.Equal(parsedBundle.Certificate.SubjectKeyId, certificates[0].AuthorityKeyId) && parsedBundle.Certificate.IsCA:
|
||||
parsedBundle.IssuingCA = parsedBundle.Certificate
|
||||
parsedBundle.IssuingCABytes = parsedBundle.CertificateBytes
|
||||
parsedBundle.CertificateBytes = pemBlock.Bytes
|
||||
parsedBundle.Certificate = certificates[0]
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
// If this case isn't correct, the caller needs to assign
|
||||
// the values to Certificate/CertificateBytes; assumptions
|
||||
// made here will not be valid for all cases.
|
||||
case certificates[0].IsCA:
|
||||
parsedBundle.IssuingCABytes = pemBlock.Bytes
|
||||
parsedBundle.IssuingCA = certificates[0]
|
||||
|
||||
default:
|
||||
parsedBundle.CertificateBytes = pemBlock.Bytes
|
||||
parsedBundle.Certificate = certificates[0]
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, UserError{"Too many certificates given; provide a maximum of two certificates in the bundle"}
|
||||
}
|
||||
}
|
||||
|
||||
if len(pemBytes) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return parsedBundle, nil
|
||||
}
|
291
helper/certutil/types.go
Normal file
291
helper/certutil/types.go
Normal file
|
@ -0,0 +1,291 @@
|
|||
// Package certutil contains helper functions that are mostly used
|
||||
// with the PKI backend but can be generally useful. Functionality
|
||||
// includes helpers for converting a certificate/private key bundle
|
||||
// between DER and PEM, printing certificate serial numbers, and more.
|
||||
//
|
||||
// Functionality specific to the PKI backend includes some types
|
||||
// and helper methods to make requesting certificates from the
|
||||
// backend easy.
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Secret is used to attempt to unmarshal a Vault secret
|
||||
// JSON response, as a convenience
|
||||
type Secret struct {
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// TLSUsage controls whether the intended usage of a *tls.Config
|
||||
// returned from ParsedCertBundle.GetTLSConfig is for server use,
|
||||
// client use, or both, which affects which values are set
|
||||
type TLSUsage int
|
||||
|
||||
// The type of of the Private Key referenced in CertBundle
|
||||
// and ParsedCertBundle. This uses colloquial names rather than
|
||||
// official names, to eliminate confusion
|
||||
const (
|
||||
UnknownPrivateKey = iota
|
||||
RSAPrivateKey
|
||||
ECPrivateKey
|
||||
|
||||
TLSUnknown TLSUsage = 0
|
||||
TLSServer TLSUsage = 1 << iota
|
||||
TLSClient
|
||||
)
|
||||
|
||||
// UserError represents an error generated due to invalid user input
|
||||
type UserError struct {
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e UserError) Error() string {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// InternalError represents an error generated internally,
|
||||
// presumably not due to invalid user input
|
||||
type InternalError struct {
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e InternalError) Error() string {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// CertBundle contains a key type, a PEM-encoded private key,
|
||||
// a PEM-encoded certificate, and a string-encoded serial number,
|
||||
// returned from a successful Issue request
|
||||
type CertBundle struct {
|
||||
PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"`
|
||||
Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
|
||||
SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"`
|
||||
}
|
||||
|
||||
// ParsedCertBundle contains a key type, a DER-encoded private key,
|
||||
// a DER-encoded certificate, and a big.Int serial number
|
||||
type ParsedCertBundle struct {
|
||||
PrivateKeyType int
|
||||
PrivateKeyBytes []byte
|
||||
PrivateKey crypto.Signer
|
||||
IssuingCABytes []byte
|
||||
IssuingCA *x509.Certificate
|
||||
CertificateBytes []byte
|
||||
Certificate *x509.Certificate
|
||||
}
|
||||
|
||||
// ToParsedCertBundle converts a string-based certificate bundle
|
||||
// to a byte-based raw certificate bundle
|
||||
func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) {
|
||||
result := &ParsedCertBundle{}
|
||||
var err error
|
||||
var pemBlock *pem.Block
|
||||
|
||||
if len(c.PrivateKey) > 0 {
|
||||
pemBlock, _ = pem.Decode([]byte(c.PrivateKey))
|
||||
if pemBlock == nil {
|
||||
return nil, UserError{"Error decoding private key from cert bundle"}
|
||||
}
|
||||
result.PrivateKeyBytes = pemBlock.Bytes
|
||||
|
||||
switch c.PrivateKeyType {
|
||||
case "ec":
|
||||
result.PrivateKeyType = ECPrivateKey
|
||||
case "rsa":
|
||||
result.PrivateKeyType = RSAPrivateKey
|
||||
default:
|
||||
// Try to figure it out and correct
|
||||
if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
|
||||
result.PrivateKeyType = ECPrivateKey
|
||||
c.PrivateKeyType = "ec"
|
||||
} else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
|
||||
result.PrivateKeyType = RSAPrivateKey
|
||||
c.PrivateKeyType = "rsa"
|
||||
} else {
|
||||
return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)}
|
||||
}
|
||||
}
|
||||
|
||||
result.PrivateKey, err = result.getSigner()
|
||||
if err != nil {
|
||||
return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Certificate) > 0 {
|
||||
pemBlock, _ = pem.Decode([]byte(c.Certificate))
|
||||
if pemBlock == nil {
|
||||
return nil, UserError{"Error decoding certificate from cert bundle"}
|
||||
}
|
||||
result.CertificateBytes = pemBlock.Bytes
|
||||
result.Certificate, err = x509.ParseCertificate(result.CertificateBytes)
|
||||
if err != nil {
|
||||
return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.IssuingCA) > 0 {
|
||||
pemBlock, _ = pem.Decode([]byte(c.IssuingCA))
|
||||
if pemBlock == nil {
|
||||
return nil, UserError{"Error decoding issuing CA from cert bundle"}
|
||||
}
|
||||
result.IssuingCABytes = pemBlock.Bytes
|
||||
result.IssuingCA, err = x509.ParseCertificate(result.IssuingCABytes)
|
||||
if err != nil {
|
||||
return nil, UserError{fmt.Sprintf("Error parsing CA certificate: %s", err)}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.SerialNumber) == 0 && len(c.Certificate) > 0 {
|
||||
c.SerialNumber = GetOctalFormatted(result.Certificate.SerialNumber.Bytes(), ":")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ToCertBundle converts a byte-based raw DER certificate bundle
|
||||
// to a PEM-based string certificate bundle
|
||||
func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) {
|
||||
result := &CertBundle{}
|
||||
block := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
}
|
||||
|
||||
if p.Certificate != nil {
|
||||
result.SerialNumber = strings.TrimSpace(GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":"))
|
||||
}
|
||||
|
||||
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
|
||||
block.Bytes = p.CertificateBytes
|
||||
result.Certificate = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
||||
}
|
||||
|
||||
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
|
||||
block.Bytes = p.IssuingCABytes
|
||||
result.IssuingCA = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
||||
}
|
||||
|
||||
if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 {
|
||||
block.Bytes = p.PrivateKeyBytes
|
||||
switch p.PrivateKeyType {
|
||||
case RSAPrivateKey:
|
||||
result.PrivateKeyType = "rsa"
|
||||
block.Type = "RSA PRIVATE KEY"
|
||||
case ECPrivateKey:
|
||||
result.PrivateKeyType = "ec"
|
||||
block.Type = "EC PRIVATE KEY"
|
||||
default:
|
||||
return nil, InternalError{"Could not determine private key type when creating block"}
|
||||
}
|
||||
result.PrivateKey = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetSigner returns a crypto.Signer corresponding to the private key
|
||||
// contained in this ParsedCertBundle. The Signer contains a Public() function
|
||||
// for getting the corresponding public. The Signer can also be
|
||||
// type-converted to private keys
|
||||
func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) {
|
||||
var signer crypto.Signer
|
||||
var err error
|
||||
|
||||
if p.PrivateKeyBytes == nil || len(p.PrivateKeyBytes) == 0 {
|
||||
return nil, UserError{"Given parsed cert bundle does not have private key information"}
|
||||
}
|
||||
|
||||
switch p.PrivateKeyType {
|
||||
case ECPrivateKey:
|
||||
signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)}
|
||||
}
|
||||
|
||||
case RSAPrivateKey:
|
||||
signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"}
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// GetTLSConfig returns a TLS config generally suitable for client
|
||||
// authentiation. The returned TLS config can be modified slightly
|
||||
// to be made suitable for a server requiring client authentication;
|
||||
// specifically, you should set the value of ClientAuth in the returned
|
||||
// config to match your needs.
|
||||
func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) {
|
||||
tlsCert := tls.Certificate{
|
||||
Certificate: [][]byte{},
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
|
||||
if p.Certificate != nil {
|
||||
tlsCert.Leaf = p.Certificate
|
||||
}
|
||||
|
||||
if p.PrivateKey != nil {
|
||||
tlsCert.PrivateKey = p.PrivateKey
|
||||
}
|
||||
|
||||
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
|
||||
tlsCert.Certificate = append(tlsCert.Certificate, p.CertificateBytes)
|
||||
}
|
||||
|
||||
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
|
||||
tlsCert.Certificate = append(tlsCert.Certificate, p.IssuingCABytes)
|
||||
|
||||
// Technically we only need one cert, but this doesn't duplicate code
|
||||
certBundle, err := p.ToCertBundle()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error converting parsed bundle to string bundle when getting TLS config: %s", err)
|
||||
}
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
ok := caPool.AppendCertsFromPEM([]byte(certBundle.IssuingCA))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Could not append CA certificate")
|
||||
}
|
||||
|
||||
if usage&TLSServer > 0 {
|
||||
tlsConfig.ClientCAs = caPool
|
||||
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
}
|
||||
if usage&TLSClient > 0 {
|
||||
tlsConfig.RootCAs = caPool
|
||||
}
|
||||
}
|
||||
|
||||
if tlsCert.Certificate != nil && len(tlsCert.Certificate) > 0 {
|
||||
tlsConfig.Certificates = []tls.Certificate{tlsCert}
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// IssueData is a structure that is suitable for marshaling into a request;
|
||||
// either via JSON, or into a map[string]interface{} via the structs package
|
||||
type IssueData struct {
|
||||
Lease string `json:"lease" structs:"lease" mapstructure:"lease"`
|
||||
CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"`
|
||||
AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"`
|
||||
IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"`
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// +build linux darwin
|
||||
// +build linux darwin freebsd
|
||||
|
||||
package password
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ employees actively contribute to Vault.
|
|||
Jack Pearkes is the creator of the online interactive demo of Vault.
|
||||
He maintains this demo as well as the design and interaction of the
|
||||
Vault website. Jack is an employee of HashiCorp and a primary engineer
|
||||
behind <a href="https//atlas.hashicorp.com">Atlas</a>.
|
||||
behind <a href="https://atlas.hashicorp.com">Atlas</a>.
|
||||
He is also a core committer to
|
||||
<a href="https://www.packer.io">Packer</a>,
|
||||
<a href="https://www.consul.io">Consul</a>, and
|
||||
|
|
649
website/source/docs/secrets/pki/index.html.md
Normal file
649
website/source/docs/secrets/pki/index.html.md
Normal file
|
@ -0,0 +1,649 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Secret Backend: PKI"
|
||||
sidebar_current: "docs-secrets-pki"
|
||||
description: |-
|
||||
The PKI secret backend for Vault generates TLS certificates.
|
||||
---
|
||||
|
||||
# PKI Secret Backend
|
||||
|
||||
Name: `pki`
|
||||
|
||||
The PKI secret backend for Vault generates X.509 certificates dynamically based on configured roles. This means services can get certificates needed for both client and server authentication without going through the usual manual process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process to complete. Vault's built-in authentication and authorization mechanisms provide the verification functionality.
|
||||
|
||||
By keeping leases relatively short, revocations are less likely to be needed, keeping CRLs short and helping the backend scale to large workloads. This in turn allows each instance of a running application to have a unique certificate, eliminating sharing and the accompanying pain of revocation and rollover.
|
||||
|
||||
In addition, by allowing revocation to mostly be forgone, this backend allows for ephemeral certificates; certificates can be fetched and stored in memory upon application startup and discarded upon shutdown, without ever being written to disk.
|
||||
|
||||
This page will show a quick start for this backend. For detailed documentation on every path, use `vault help` after mounting the backend.
|
||||
|
||||
## Considerations
|
||||
|
||||
To successfully deploy this backend, there are a number of important considerations to be aware of, as well as some preparatory steps that should be undertaken. You should read all of these *before* using this backend or generating the CA to use with this backend.
|
||||
|
||||
### Never use root CAs
|
||||
|
||||
Vault storage is secure, but not as secure as a piece of paper in a bank vault. It is, after all, networked software. Your long-lived self-signed root CA's private key should instead be used to issue a shorter-lived intermediate CA certificate, and this is what you should put into Vault. This aligns with industry best practices.
|
||||
|
||||
### One CA Certificate, One Backend
|
||||
|
||||
In order to vastly simplify both the configuration and codebase of the PKI backend, only one CA certificate is allowed per backend. If you want to issue certificates from multiple CAs, mount the PKI backend at multiple mount points with separate CA certificates in each.
|
||||
|
||||
This also provides a convenient method of switching to a new CA certificate while keeping CRLs valid from the old CA certificate; simply mount a new backend and issue from there.
|
||||
|
||||
### Keep certificate lifetimes short, for CRL's sake
|
||||
|
||||
This backend aligns with Vault's philosophy of short-lived secrets. As such it is not expected that CRLs will grow large; the only place a private key is ever returned is to the requesting client (this backend does *not* store generated private keys). In most cases, if the key is lost, the certificate can simply be ignored, as it will expire shortly.
|
||||
|
||||
If a certificate must truly be revoked, the normal Vault revocation function can be used; alternately a root token can be used to revoke the certificate using the certificate's serial number. Any revocation action will cause the CRL to be regenerated. When the CRL is regenerated, any expired certificates are removed from the CRL (and any revoked, expired certificate are removed from backend storage).
|
||||
|
||||
This backend does not support multiple CRL endpoints with sliding date windows; often such mechanisms will have the transition point a few days apart, but this gets into the expected realm of the actual certificate validity periods issued from this backend. A good rule of thumb for this backend would be to simply not issue certificates with a validity period greater than your maximum comfortable CRL lifetime. Alternately, you can control CRL caching behavior on the client to ensure that checks happen more often.
|
||||
|
||||
Often multiple endpoints are used in case a single CRL endpoint is down so that clients don't have to figure out what to do with a lack of response. Run Vault in HA mode, and the CRL endpoint should be available even if a particular node is down.
|
||||
|
||||
### You must configure CRL information *in advance*
|
||||
|
||||
This backend serves CRLs from a predictable location. That location must be encoded into your CA certificate if you want to allow applications to use the CRL endpoint encoded in certificates to find the CRL. Instructions for doing so are below. If you need to adjust this later, you will have to generate a new CA certificate using the same private key if you want to keep validity for already-issued certificates.
|
||||
|
||||
### No OCSP support, yet
|
||||
|
||||
Vault's architecture does not currently allow for a binary protocol such as OCSP to be supported by a backend. As such, you should configure your software to use CRLs for revocation information, with a caching lifetime that feels good to you. Since you are following the advice above about keeping lifetimes short (right?), CRLs should not grow too large.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CA certificate
|
||||
|
||||
In order for this backend to serve CRL information at the expected location, you will need to generate your CA certificate with this information. For OpenSSL, this means putting a value in the CA section with the appropriate URL; in this example the PKI backend is mounted at `pki`:
|
||||
|
||||
```text
|
||||
crlDistributionPoints = URI:https://vault.example.com:8200/v1/pki/crl
|
||||
```
|
||||
|
||||
Adjust the URI as appropriate.
|
||||
|
||||
### Vault
|
||||
|
||||
The first step to using the PKI backend is to mount it. Unlike the `generic` backend, the `pki` backend is not mounted by default.
|
||||
|
||||
```text
|
||||
$ vault mount pki
|
||||
Successfully mounted 'pki' at 'pki'!
|
||||
```
|
||||
|
||||
Next, Vault must be configured with a root certificate and associated private key. This is done by writing the contents of a file or *stdin*:
|
||||
|
||||
```text
|
||||
$ vault write pki/config/ca pem_bundle="@ca_bundle.pem"
|
||||
Success! Data written to: pki/config/ca
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
$ cat bundle.pem | vault write pki/config/ca pem_bundle="-"
|
||||
Success! Data written to: pki/config/ca
|
||||
```
|
||||
|
||||
Although in this example the value being piped into *stdin* could be passed directly into the Vault CLI command, a more complex usage might be to use [Ansible](http://www.ansible.com) to securely store the certificate and private key in an `ansible-vault` file, then have an `ansible-playbook` command decrypt this value and pass it in to Vault.
|
||||
|
||||
The next step is to configure a role. A role is a logical name that maps to a policy used to generated those credentials. For example, let's create an "example-dot-com" role:
|
||||
|
||||
```text
|
||||
$ vault write pki/roles/example-dot-com \
|
||||
allowed_base_domain="example.com" \
|
||||
allow_subdomains="true" lease_max="72h"
|
||||
Success! Data written to: pki/roles/example-dot-com
|
||||
```
|
||||
|
||||
By writing to the `roles/example-dot-com` path we are defining the `example-dot-com` role. To generate a new set of credentials, we simply write to the `issue` endpoint with that role name: Vault is now configured to create and manage certificates!
|
||||
|
||||
```text
|
||||
$ vault write pki/issue/example-dot-com common_name=blah.example.com
|
||||
Key Value
|
||||
lease_id pki/issue/example-dot-com/819393b5-e1a1-9efd-b72f-4dc3a1972e31
|
||||
lease_duration 259200
|
||||
lease_renewable false
|
||||
certificate -----BEGIN CERTIFICATE-----
|
||||
MIIECDCCAvKgAwIBAgIUXmLrLkTdBIOOIYg2/BXO7docKfUwCwYJKoZIhvcNAQEL
|
||||
...
|
||||
az3gfwlOqVTdgi/ZVAtIzhSEJ0OY136bq4NOaw==
|
||||
-----END CERTIFICATE-----
|
||||
issuing_ca -----BEGIN CERTIFICATE-----
|
||||
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
...
|
||||
-----END CERTIFICATE-----
|
||||
private_key -----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA0cczc7Y2yIu7aD/IaDi23Io+tvvDS9XaXXDUFW1kqd58P83r
|
||||
...
|
||||
3xhCNnZ3CMQaM2I48sloVK/XoikMLb5MZwOUQn/V+TrhWP4Lu7qD
|
||||
-----END RSA PRIVATE KEY-----
|
||||
serial 5e:62:eb:2e:44:dd:04:83:8e:21:88:36:fc:15:ce:ed:da:1c:29:f5
|
||||
```
|
||||
|
||||
Note that this is a write, not a read, to allow values to be passed in at request time.
|
||||
|
||||
Vault has now generated a new set of credentials using the `example-dot-com` role configuration. Here we see the dynamically generated private key and certificate. The issuing CA certificate is returned as well.
|
||||
|
||||
Using ACLs, it is possible to restrict using the pki backend such that trusted operators can manage the role definitions, and both users and applications are restricted in the credentials they are allowed to read.
|
||||
|
||||
If you get stuck at any time, simply run `vault help pki` or with a subpath for interactive help output.
|
||||
|
||||
## API
|
||||
|
||||
### /pki/ca(/pem)
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Retrieves the CA certificate *in raw DER-encoded form*.
|
||||
This is a bare endpoint that does not return a
|
||||
standard Vault data structure. If `/pem` is added to the
|
||||
endpoint, the CA certificate is returned in PEM format.
|
||||
<br /><br />This is an unauthenticated endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/ca(/pem)`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```
|
||||
<binary DER-encoded certficiate>
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/cert/
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Retrieves one of a selection of certificates. Valid values: `ca`
|
||||
for the CA certificate, `crl` for the current CRL, or a serial
|
||||
number in either hyphen-separated or colon-separated octal format.
|
||||
This endpoint returns the certificate in PEM formatting in the
|
||||
`certificate` key of the JSON object.
|
||||
<br /><br />This is an unauthenticated endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/cert/<serial>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..."
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/config/ca
|
||||
#### POST
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
A PEM file containing the issuing CA certificate
|
||||
and its private key, concatenated.
|
||||
<br /><br />This is a root-protected endpoint.
|
||||
<br /><br />The information can be provided from a file via a `curl`
|
||||
command similar to the following:<br/>
|
||||
|
||||
```text
|
||||
curl -X POST --data "@cabundle.json" http://127.0.0.1:8200/v1/pki/config/ca -H X-Vault-Token:06b9d...
|
||||
```
|
||||
|
||||
Note that if you provide the data through the HTTP API it must be
|
||||
JSON-formatted, with newlines replaced with `\n`, like so:
|
||||
|
||||
```text
|
||||
{ "pem_bundle": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END CERTIFICATE-----" }
|
||||
```
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>POST</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/config/ca`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="param">pem_bundle</span>
|
||||
<span class="param-flags">required</span>
|
||||
The key and certificate concatenated in PEM format.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
A `204` response code.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/crl(/pem)
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Retrieves the current CRL *in raw DER-encoded form*. This endpoint
|
||||
is suitable for usage in the CRL Distribution Points extension in a
|
||||
CA certificate. This is a bare endpoint that does not return a
|
||||
standard Vault data structure. If `/pem` is added to the endpoint,
|
||||
the CRL is returned in PEM format.
|
||||
<br /><br />This is an unauthenticated endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/crl(/pem)`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```
|
||||
<binary DER-encoded CRL>
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/crl/rotate
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
This endpoint forces a rotation of the CRL. This can be used
|
||||
by administrators to cut the size of the CRL if it contains
|
||||
a number of certificates that have now expired, but has
|
||||
not been rotated due to no further certificates being revoked.
|
||||
<br /><br />This is a root-protected endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/crl/rotate`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/issue/
|
||||
#### POST
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Generates a new set of credentials (private key and
|
||||
certificate) based on the named role. The issuing CA
|
||||
certificate is returned as well, so that only the root CA
|
||||
need be in a client's trust store.
|
||||
<br /><br />*The private key is _not_ stored.
|
||||
If you do not save the private key, you will need to
|
||||
request a new certificate.*
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>POST</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/issue/<name>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="param">common_name</span>
|
||||
<span class="param-flags">required</span>
|
||||
The requested CN for the certificate. If the CN is allowed
|
||||
by role policy, it will be issued.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">alt_names</span>
|
||||
<span class="param-flags">optional</span>
|
||||
Requested Subject Alternative Names, in a comma-delimited
|
||||
list. If any requested names do not match role policy,
|
||||
the entire request will be denied.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">ip_sans</span>
|
||||
<span class="param-flags">optional</span>
|
||||
Requested IP Subject Alternative Names, in a comma-delimited
|
||||
list. Only valid if the role allows IP SANs (which is the
|
||||
default).
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">lease</span>
|
||||
<span class="param-flags">optional</span>
|
||||
Requested lease time. Cannot be greater than the role's
|
||||
`lease_max` parameter. If not provided, the role's `lease`
|
||||
value will be used.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"lease_id": "pki/issue/test/7ad6cfa5-f04f-c62a-d477-f33210475d05",
|
||||
"renewable": false,
|
||||
"lease_duration": 21600,
|
||||
"data": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n",
|
||||
"issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAnVHfwoKsUG1GDVyWB1AFroaKl2ImMBO8EnvGLRrmobIkQvh+\n...\nQN351pgTphi6nlCkGPzkDuwvtxSxiCWXQcaxrHAL7MiJpPzkIBq1\n-----END RSA PRIVATE KEY-----\n",
|
||||
"serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58"
|
||||
},
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/revoke
|
||||
#### POST
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Revokes a certificate using its serial number. This is an
|
||||
alternative option to the standard method of revoking
|
||||
using Vault lease IDs. A successful revocation will
|
||||
rotate the CRL.
|
||||
<br /><br />This is a root-protected endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>POST</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/revoke`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="param">serial</span>
|
||||
<span class="param-flags">required</span>
|
||||
The serial number of the certificate to revoke, in
|
||||
hyphen-separated or colon-separated octal.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"revocation_time": 1433269787
|
||||
}
|
||||
}
|
||||
```
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /pki/roles/
|
||||
#### POST
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Creates or updates the role definition. Note that
|
||||
the `allowed_base_domain`, `allow_token_displayname`,
|
||||
`allow_subdomains`, and `allow_any_name` attributes
|
||||
are additive; between them nearly and across multiple
|
||||
roles nearly any issuing policy can be accommodated.
|
||||
`server_flag`, `client_flag`, and `code_signing_flag`
|
||||
are additive as well. If a client requests a
|
||||
certificate that is not allowed by the CN policy in
|
||||
the role, the request is denied.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>POST</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/roles/<name>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="param">lease</span>
|
||||
<span class="param-flags">optional</span>
|
||||
The lease value provided as a string duration
|
||||
with time suffix. Hour is the largest suffix.
|
||||
If not set, uses the value of `lease_max`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">lease_max</span>
|
||||
<span class="param-flags">required</span>
|
||||
The maximum lease value provided as a string duration
|
||||
with time suffix. Hour is the largest suffix.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allow_localhost</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request certificates for `localhost`
|
||||
as one of the requested common names. This is useful
|
||||
for testing and to allow clients on a single host to
|
||||
talk securely.
|
||||
Defaults to true.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allowed_base_domain</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request certificates for subdomains
|
||||
directly off of this base domain. _This includes the
|
||||
wildcard subdomain._ For instance, a base_domain of
|
||||
`example.com` allows clients to request certificates for
|
||||
`foo.example.com` and `*.example.com`. To allow further
|
||||
levels of subdomains, enable the `allow_subdomains` option.
|
||||
There is no default.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allow_token_displayname</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request certificates matching
|
||||
the value of Display Name from the requesting token.
|
||||
Remember, this stacks with the other CN options,
|
||||
including `allowed_base_domain`. Defaults to `false`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allow_subdomains</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request certificates with CNs that
|
||||
are subdomains of the CNs allowed by the other role
|
||||
options. _This includes wildcard subdomains._ This is
|
||||
redundant when using the `allow_any_name` option.
|
||||
Defaults to `false`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allow_any_name</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request any CN. Useful in some
|
||||
circumstances, but make sure you understand whether it
|
||||
is appropriate for your installation before enabling it.
|
||||
Defaults to `false`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">allow_ip_sans</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, clients can request IP Subject Alternative
|
||||
Names. Unlike CNs, no authorization checking is
|
||||
performed except to verify that the given values
|
||||
are valid IP addresses. Defaults to `true`.
|
||||
<li>
|
||||
<span class="param">server_flag</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, certificates are flagged for server use.
|
||||
Defaults to `true`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">client_flag</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, certificates are flagged for client use.
|
||||
Defaults to `true`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">code_signing_flag</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, certificates are flagged for code signing
|
||||
use. Defaults to `false`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">key_type</span>
|
||||
<span class="param-flags">optional</span>
|
||||
The type of key to generate for generated private
|
||||
keys. Currently, `rsa` and `ec` are supported.
|
||||
Defaults to `rsa`.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">key_bits</span>
|
||||
<span class="param-flags">optional</span>
|
||||
The number of bits to use for the generated keys.
|
||||
Defaults to `2048`; this will need to be changed for
|
||||
`ec` keys. See https://golang.org/pkg/crypto/elliptic/#Curve
|
||||
for an overview of allowed bit lengths for `ec`.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
A `204` response code.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Queries the role definition.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/roles/<name>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"allow_any_name": false,
|
||||
"allow_ip_sans": true,
|
||||
"allow_localhost": true,
|
||||
"allow_subdomains": false,
|
||||
"allow_token_displayname": false,
|
||||
"allowed_base_domain": "example.com",
|
||||
"client_flag": true,
|
||||
"code_signing_flag": false,
|
||||
"key_bits": 2048,
|
||||
"key_type": "rsa",
|
||||
"lease": "6h",
|
||||
"lease_max": "12h",
|
||||
"server_flag": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
#### DELETE
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Deletes the role definition. Deleting a role does <b>not</b> revoke
|
||||
certificates previously issued under this role.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>DELETE</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/pki/roles/<name>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
A `204` response code.
|
||||
</dd>
|
||||
</dl>
|
|
@ -106,6 +106,10 @@
|
|||
<a href="/docs/secrets/consul/index.html">Consul</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-secrets-pki") %>>
|
||||
<a href="/docs/secrets/pki/index.html">PKI (Certificates)</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-secrets-postgresql") %>>
|
||||
<a href="/docs/secrets/postgresql/index.html">PostgreSQL</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in a new issue