Re-add upsert into transit. Defaults to off and a new endpoint /config

can be used to turn it on for a given mount.
This commit is contained in:
Jeff Mitchell
2016-02-01 20:13:57 -05:00
parent d402292f85
commit dc27d012c0
7 changed files with 336 additions and 101 deletions

View File

@@ -19,9 +19,10 @@ func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Paths: []*framework.Path{
b.pathConfig(),
// Rotate/Config needs to come before Keys
// as the handler is greedy
b.pathConfig(),
b.pathKeysConfig(),
b.pathRotate(),
b.pathRewrap(),
b.pathKeys(),

View File

@@ -111,6 +111,29 @@ func TestBackend_rotation(t *testing.T) {
})
}
func TestBackend_upsert(t *testing.T) {
decryptData := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory,
Steps: []logicaltest.TestStep{
testAccStepReadPolicy(t, "test", true, false),
testAccStepEncryptExpectFailure(t, "test", testPlaintext, decryptData),
testAccStepReadPolicy(t, "test", true, false),
testAccStepConfigUpsert(t, true),
testAccStepEncrypt(t, "test", testPlaintext, decryptData),
testAccStepReadPolicy(t, "test", false, false),
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
testAccStepConfigUpsert(t, false),
testAccStepReadPolicy(t, "test2", true, false),
testAccStepEncryptExpectFailure(t, "test2", testPlaintext, decryptData),
testAccStepReadPolicy(t, "test2", true, false),
testAccStepEnableDeletion(t, "test"),
testAccStepDeletePolicy(t, "test"),
testAccStepReadPolicy(t, "test", true, false),
},
})
}
func TestBackend_basic_derived(t *testing.T) {
decryptData := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
@@ -268,6 +291,24 @@ func testAccStepEncrypt(
}
}
func testAccStepEncryptExpectFailure(
t *testing.T, name, plaintext string, decryptData map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "encrypt/" + name,
Data: map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)),
},
ErrorOk: true,
Check: func(resp *logical.Response) error {
if !resp.IsError() {
return fmt.Errorf("expected error")
}
return nil
},
}
}
func testAccStepEncryptContext(
t *testing.T, name, plaintext, context string, decryptData map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
@@ -488,6 +529,16 @@ func testAccStepDecryptDatakey(t *testing.T, name string,
}
}
func testAccStepConfigUpsert(t *testing.T, upsert bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"allow_upsert": upsert,
},
}
}
func TestKeyUpgrade(t *testing.T) {
p := &Policy{
Name: "test",

View File

@@ -1,34 +1,26 @@
package transit
import (
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func (b *backend) pathConfig() *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name") + "/config",
Pattern: "config",
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the key",
},
"min_decryption_version": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `If set, the minimum version of the key allowed
to be decrypted.`,
},
"deletion_allowed": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Whether to allow deletion of the key",
"allow_upsert": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `Whether to allow upserting keys on first use,
rather than requiring them to be manually
specified through the keys endpoint`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigRead,
logical.UpdateOperation: b.pathConfigWrite,
},
@@ -37,85 +29,69 @@ to be decrypted.`,
}
}
func (b *backend) pathConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
type transitConfig struct {
AllowUpsert bool `json:"allow_upsert" structs:"allow_upsert" mapstructure:"allow_upsert"`
}
// Check if the policy already exists
lp, err := b.policies.getPolicy(req, name)
func (b *backend) getConfig(s logical.Storage) (*transitConfig, error) {
entry, err := s.Get("config")
if err != nil {
return nil, err
}
if lp == nil {
return logical.ErrorResponse(
fmt.Sprintf("no existing role named %s could be found", name)),
logical.ErrInvalidRequest
}
lp.Lock()
defer lp.Unlock()
// Verify if wasn't deleted before we grabbed the lock
if lp.policy == nil {
return nil, fmt.Errorf("no existing role named %s could be found", name)
}
resp := &logical.Response{}
persistNeeded := false
minDecryptionVersionRaw, ok := d.GetOk("min_decryption_version")
if ok {
minDecryptionVersion := minDecryptionVersionRaw.(int)
if minDecryptionVersion < 0 {
return logical.ErrorResponse("min decryption version cannot be negative"), nil
}
if minDecryptionVersion == 0 {
minDecryptionVersion = 1
resp.AddWarning("since Vault 0.3, transit key numbering starts at 1; forcing minimum to 1")
}
if minDecryptionVersion > 0 &&
minDecryptionVersion != lp.policy.MinDecryptionVersion {
if minDecryptionVersion > lp.policy.LatestVersion {
return logical.ErrorResponse(
fmt.Sprintf("cannot set min decryption version of %d, latest key version is %d", minDecryptionVersion, lp.policy.LatestVersion)), nil
}
lp.policy.MinDecryptionVersion = minDecryptionVersion
persistNeeded = true
}
}
allowDeletionInt, ok := d.GetOk("deletion_allowed")
if ok {
allowDeletion := allowDeletionInt.(bool)
if allowDeletion != lp.policy.DeletionAllowed {
lp.policy.DeletionAllowed = allowDeletion
persistNeeded = true
}
}
// Add this as a guard here before persisting since we now require the min
// decryption version to start at 1; even if it's not explicitly set here,
// force the upgrade
if lp.policy.MinDecryptionVersion == 0 {
lp.policy.MinDecryptionVersion = 1
persistNeeded = true
}
if !persistNeeded {
if entry == nil {
return nil, nil
}
return resp, lp.policy.Persist(req.Storage)
var result transitConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
const pathConfigHelpSyn = `Configure a named encryption key`
func (b *backend) pathConfigRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.getConfig(req.Storage)
if err != nil {
return nil, err
}
if config == nil {
config = &transitConfig{}
}
resp := &logical.Response{
Data: structs.New(config).Map(),
}
return resp, nil
}
func (b *backend) pathConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
allowUpsertInt, ok := d.GetOk("allow_upsert")
if !ok {
return logical.ErrorResponse("no known configuration parameters supplied"), nil
}
config := &transitConfig{
AllowUpsert: allowUpsertInt.(bool),
}
jsonEntry, err := logical.StorageEntryJSON("config", config)
if err != nil {
return nil, err
}
if err := req.Storage.Put(jsonEntry); err != nil {
return nil, err
}
return nil, nil
}
const pathConfigHelpSyn = `Configure the backend`
const pathConfigHelpDesc = `
This path is used to configure the named key. Currently, this
supports adjusting the minimum version of the key allowed to
be used for decryption via the min_decryption_version paramter.
`
This path is used to configure the backend. Currently, this allows configuring
whether or not keys can be created via upsert from the encryption endpoint.`

View File

@@ -65,7 +65,24 @@ func (b *backend) pathEncryptWrite(
// Error if invalid policy
if lp == nil {
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
config, err := b.getConfig(req.Storage)
if err != nil {
return nil, err
}
if config == nil || !config.AllowUpsert {
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
}
isDerived := len(context) != 0
lp, err = b.policies.generatePolicy(req.Storage, name, isDerived)
// If the error is that the policy has been created in the interim we
// will get the policy back, so only consider it an error if err is not
// nil and we do not get a policy back
if err != nil && lp != nil {
return nil, err
}
}
lp.RLock()

View File

@@ -0,0 +1,121 @@
package transit
import (
"fmt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func (b *backend) pathKeysConfig() *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name") + "/config",
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the key",
},
"min_decryption_version": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `If set, the minimum version of the key allowed
to be decrypted.`,
},
"deletion_allowed": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Whether to allow deletion of the key",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathKeysConfigWrite,
},
HelpSynopsis: pathKeysConfigHelpSyn,
HelpDescription: pathKeysConfigHelpDesc,
}
}
func (b *backend) pathKeysConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
// Check if the policy already exists
lp, err := b.policies.getPolicy(req, name)
if err != nil {
return nil, err
}
if lp == nil {
return logical.ErrorResponse(
fmt.Sprintf("no existing role named %s could be found", name)),
logical.ErrInvalidRequest
}
lp.Lock()
defer lp.Unlock()
// Verify if wasn't deleted before we grabbed the lock
if lp.policy == nil {
return nil, fmt.Errorf("no existing role named %s could be found", name)
}
resp := &logical.Response{}
persistNeeded := false
minDecryptionVersionRaw, ok := d.GetOk("min_decryption_version")
if ok {
minDecryptionVersion := minDecryptionVersionRaw.(int)
if minDecryptionVersion < 0 {
return logical.ErrorResponse("min decryption version cannot be negative"), nil
}
if minDecryptionVersion == 0 {
minDecryptionVersion = 1
resp.AddWarning("since Vault 0.3, transit key numbering starts at 1; forcing minimum to 1")
}
if minDecryptionVersion > 0 &&
minDecryptionVersion != lp.policy.MinDecryptionVersion {
if minDecryptionVersion > lp.policy.LatestVersion {
return logical.ErrorResponse(
fmt.Sprintf("cannot set min decryption version of %d, latest key version is %d", minDecryptionVersion, lp.policy.LatestVersion)), nil
}
lp.policy.MinDecryptionVersion = minDecryptionVersion
persistNeeded = true
}
}
allowDeletionInt, ok := d.GetOk("deletion_allowed")
if ok {
allowDeletion := allowDeletionInt.(bool)
if allowDeletion != lp.policy.DeletionAllowed {
lp.policy.DeletionAllowed = allowDeletion
persistNeeded = true
}
}
// Add this as a guard here before persisting since we now require the min
// decryption version to start at 1; even if it's not explicitly set here,
// force the upgrade
if lp.policy.MinDecryptionVersion == 0 {
lp.policy.MinDecryptionVersion = 1
persistNeeded = true
}
if !persistNeeded {
return nil, nil
}
return resp, lp.policy.Persist(req.Storage)
}
const pathKeysConfigHelpSyn = `Configure a named encryption key`
const pathKeysConfigHelpDesc = `
This path is used to configure the named key. Currently, this
supports adjusting the minimum version of the key allowed to
be used for decryption via the min_decryption_version paramter.
`

View File

@@ -134,7 +134,7 @@ func (p *policyCache) generatePolicy(storage logical.Storage, name string, deriv
// created since we checked getPolicy. A policy being created holds a write
// lock until it's done, so it'll be in the cache at this point.
if lp := p.cache[name]; lp != nil {
return nil, fmt.Errorf("policy %s already exists", name)
return lp, fmt.Errorf("policy %s already exists", name)
}
// Create the policy object

View File

@@ -123,13 +123,81 @@ only encrypt or decrypt using the named keys they need access to.
## API
### /transit/config
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Allows setting backend configuration options.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/transit/config`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">allow_upsert</span>
<span class="param-flags">optional</span>
Boolean flag indicating if upserting of keys is allowed. If true, if a
key with the given name does not exist when a call to
`/transit/encrypt/<name>` is made, the key will be created on-the-fly.
Defaults to false.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Returns the current backend configuration.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/transit/config`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"allow_upsert": true
}
}
```
</dd>
</dl>
### /transit/keys/
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Creates a new named encryption key. This is a root protected endpoint.
Creates a new named encryption key.
</dd>
<dt>Method</dt>
@@ -165,7 +233,7 @@ only encrypt or decrypt using the named keys they need access to.
<dd>
Returns information about a named encryption key. The `keys` object shows
the creation time of each key version; the values are not the keys
themselves. This is a root protected endpoint.
themselves.
</dd>
<dt>Method</dt>
@@ -205,10 +273,10 @@ only encrypt or decrypt using the named keys they need access to.
<dl class="api">
<dt>Description</dt>
<dd>
Deletes a named encryption key. This is a root protected endpoint.
It will no longer be possible to decrypt any data encrypted with the
named key. Because this is a potentially catastrophic operation, the
`deletion_allowed` tunable must be set in the key's `/config` endpoint.
Deletes a named encryption key. It will no longer be possible to decrypt
any data encrypted with the named key. Because this is a potentially
catastrophic operation, the `deletion_allowed` tunable must be set in the
key's `/config` endpoint.
</dd>
<dt>Method</dt>
@@ -235,8 +303,7 @@ only encrypt or decrypt using the named keys they need access to.
<dt>Description</dt>
<dd>
Allows tuning configuration values for a given key. (These values are
returned during a read operation on the named key.) This is a
root-protected endpoint.
returned during a read operation on the named key.)
</dd>
<dt>Method</dt>
@@ -279,7 +346,7 @@ only encrypt or decrypt using the named keys they need access to.
Rotates the version of the named key. After rotation, new plaintext
requests will be encrypted with the new version of the key. To upgrade
ciphertext to be encrypted with the latest version of the key, use the
`rewrap` endpoint. This is a root-protected endpoint.
`rewrap` endpoint.
</dd>
<dt>Method</dt>
@@ -305,7 +372,9 @@ only encrypt or decrypt using the named keys they need access to.
<dl class="api">
<dt>Description</dt>
<dd>
Encrypts the provided plaintext using the named key.
Encrypts the provided plaintext using the named key. If `allow_upsert` is
enabled in the backend config, if the named key does not exist, it will be
created on-the-fly.
</dd>
<dt>Method</dt>