Files
threshold/internal/host/plugin/host_catalog_secret.go
T
Chris MarchesiandTodd 0670e272e3 internal/host/plugin: Add UpdateCatalog (#1634)
* internal/host/plugin: Add UpdateCatalog

This adds support for updating plugin-based host catalogs.

The workflow is:

* The existing catalog is fetched.
* Any attribute changes are patched against it, along with other changes
  such as name and description.
* Both the existing catalog, and the state of the catalog as it will
  exist post-update, along with any persisted secrets, are sent to the
  plugin via an OnUpdateCatalog request.

Upon success, the following is done:

* The record is updated if non-secret updates need to be made.
* Secrets are updated if they still exist.
* If an empty secret set was sent back, secrets are instead deleted.
* If the secrets field was instead null, secret update is skipped
  altogether.

There has also been a lot of helper code added to the tests to help make
the update tests more readable and reduce boilerplate. While the create
tests have not been updated to use these new helpers, it might be a good
idea to do so for consistency.

* move secret/persisted state closer to plugin call

* Update internal/host/plugin/repository_host_catalog.go

Co-authored-by: Todd <T.Alan.Knight@gmail.com>

* Update internal/host/plugin/repository_host_catalog_test.go

Co-authored-by: Todd <T.Alan.Knight@gmail.com>

* move all test helpers for UpdateCatalog inline

* WIP - transparent attributes struct/wire conversion

* Revert "WIP - transparent attributes struct/wire conversion"

This reverts commit 31af1b1b7e535119aa23bd0aa936da1a80eb5aa2.

* deal with multiple attribute masks

* Note TODO for upsertQuery for HostCatalogSecret

* add version check guard

* Update internal/host/plugin/host_catalog_secret.go

Co-authored-by: Todd <T.Alan.Knight@gmail.com>

* remove redundant JIRA reference

* don't separate number of updated catalogs and secrets

* increment version when secrets is the only thing being updated

* add another test for updating both catalog and secrets at once

* correct comment for mock plugin manager section

* add first-part path matching for secrets

* update plugin lookup error to return errors.Internal

* only add name/description to plugin request if they are non-empty

* update duplicate tests to use two project scopes

* remove unused test checks

* return plugin record along with host catalog in update

* move away from using passed in catalog after patching newCatalog

* use t.Cleanup() instead of defer

Co-authored-by: Todd <T.Alan.Knight@gmail.com>
2021-11-05 10:43:58 -07:00

131 lines
3.8 KiB
Go

package plugin
import (
"context"
"database/sql"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/plugin/store"
"github.com/hashicorp/boundary/internal/oplog"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/structwrapping"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
// HostCatalogSecret contains the encrypted secret for a host catalog.
// It is owned by a HostCatalog.
type HostCatalogSecret struct {
*store.HostCatalogSecret
tableName string `gorm:"-"`
}
// newHostCatalogSecret creates an in memory host catalog secret.
// All options are ignored.
func newHostCatalogSecret(ctx context.Context, catalogId string, secret *structpb.Struct, _ ...Option) (*HostCatalogSecret, error) {
const op = "plugin.newHostCatlogSecret"
hcs := &HostCatalogSecret{
HostCatalogSecret: &store.HostCatalogSecret{
CatalogId: catalogId,
},
}
if secret != nil {
attrs, err := proto.Marshal(secret)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
hcs.Secret = attrs
}
return hcs, nil
}
func allocHostCatalogSecret() *HostCatalogSecret {
return &HostCatalogSecret{
HostCatalogSecret: &store.HostCatalogSecret{},
}
}
func (c *HostCatalogSecret) clone() *HostCatalogSecret {
cp := proto.Clone(c.HostCatalogSecret)
return &HostCatalogSecret{
HostCatalogSecret: cp.(*store.HostCatalogSecret),
}
}
// TableName returns the table name for the host catalog.
func (c *HostCatalogSecret) TableName() string {
if c.tableName != "" {
return c.tableName
}
return "host_plugin_catalog_secret"
}
// SetTableName sets the table name. If the caller attempts to
// set the name to "" the name will be reset to the default name.
func (c *HostCatalogSecret) SetTableName(n string) {
c.tableName = n
}
func (c *HostCatalogSecret) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "plugin.(HostCatalogSecret).encrypt"
if len(c.Secret) == 0 {
return errors.New(ctx, errors.InvalidParameter, op, "no attributes defined")
}
if err := structwrapping.WrapStruct(ctx, cipher, c.HostCatalogSecret, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
c.KeyId = cipher.KeyID()
c.Secret = nil
return nil
}
func (c *HostCatalogSecret) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "plugin.(HostCatalogSecret).decrypt"
if err := structwrapping.UnwrapStruct(ctx, cipher, c.HostCatalogSecret, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
c.CtSecret = nil
return nil
}
// TODO(ICU-2835): this is necessary as update logic requires a static
// public_id or private_id field. Once post-lookup updates can
// support alternate primary key fields, we can port this to native
// Update and upserts w/OnConflict.
func (c *HostCatalogSecret) upsertQuery() (query string, queryValues []interface{}) {
query = upsertHostCatalogSecretQuery
queryValues = []interface{}{
sql.Named("catalog_id", c.CatalogId),
sql.Named("secret", c.CtSecret),
sql.Named("key_id", c.KeyId),
}
return
}
func (c *HostCatalogSecret) deleteQuery() (query string, queryValues []interface{}) {
query = deleteHostCatalogSecretQuery
queryValues = []interface{}{
sql.Named("catalog_id", c.CatalogId),
}
return
}
func (c *HostCatalogSecret) oplogMessage(opType db.OpType) *oplog.Message {
msg := oplog.Message{
Message: c.clone(),
TypeName: c.TableName(),
}
switch opType {
case db.CreateOp:
msg.OpType = oplog.OpType_OP_TYPE_CREATE
case db.UpdateOp:
msg.OpType = oplog.OpType_OP_TYPE_UPDATE
msg.FieldMaskPaths = []string{"secret", "key_id"}
case db.DeleteOp:
msg.OpType = oplog.OpType_OP_TYPE_DELETE
}
return &msg
}