mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
713 lines
25 KiB
713 lines
25 KiB
// Copyright IBM Corp. 2020, 2025
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db/timestamp"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/oplog"
|
|
"github.com/hashicorp/boundary/internal/scheduler"
|
|
"github.com/hashicorp/go-dbw"
|
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
|
vault "github.com/hashicorp/vault/api"
|
|
)
|
|
|
|
// CreateCredentialStore inserts cs into the repository and returns a new
|
|
// CredentialStore containing the credential store's PublicId. cs is not
|
|
// changed. cs must not contain a PublicId. The PublicId is generated and
|
|
// assigned by this method. cs must contain a valid ProjectId, VaultAddress,
|
|
// and Vault token. The Vault token must be renewable, periodic, and
|
|
// orphan. CreateCredentialStore calls the /auth/token/renew-self and
|
|
// /auth/token/lookup-self Vault endpoints.
|
|
//
|
|
// Both cs.Name and cs.Description are optional. If cs.Name is set, it must
|
|
// be unique within cs.ProjectId. Both cs.CreateTime and cs.UpdateTime are
|
|
// ignored.
|
|
//
|
|
// For more information about the required properties of the Vault token see:
|
|
// https://www.vaultproject.io/api-docs/auth/token#period,
|
|
// https://www.vaultproject.io/api-docs/auth/token#renewable,
|
|
// https://www.vaultproject.io/docs/concepts/tokens#token-hierarchies-and-orphan-tokens,
|
|
// https://www.vaultproject.io/docs/concepts/tokens#periodic-tokens, and
|
|
// https://www.vaultproject.io/docs/concepts/tokens#token-time-to-live-periodic-tokens-and-explicit-max-ttls.
|
|
//
|
|
// For more information about the Vault endpoints called by
|
|
// CreateCredentialStore see:
|
|
// https://www.vaultproject.io/api-docs/auth/token#renew-a-token-self and
|
|
// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self.
|
|
func (r *Repository) CreateCredentialStore(ctx context.Context, cs *CredentialStore, _ ...Option) (*CredentialStore, error) {
|
|
const op = "vault.(Repository).CreateCredentialStore"
|
|
if cs == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil CredentialStore")
|
|
}
|
|
if cs.CredentialStore == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded CredentialStore")
|
|
}
|
|
if cs.ProjectId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id")
|
|
}
|
|
if len(cs.inputToken) == 0 {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no vault token")
|
|
}
|
|
if cs.VaultAddress == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no vault address")
|
|
}
|
|
if cs.PublicId != "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty")
|
|
}
|
|
if cs.clientCert != nil && len(cs.clientCert.CertificateKey) == 0 {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "client certificate without private key")
|
|
}
|
|
|
|
cs = cs.clone()
|
|
|
|
id, err := newCredentialStoreId(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
cs.PublicId = id
|
|
if cs.clientCert != nil {
|
|
cs.clientCert.StoreId = id
|
|
}
|
|
|
|
client, err := cs.client(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create vault client"))
|
|
}
|
|
tokenLookup, err := client.lookupToken(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup vault token"))
|
|
}
|
|
if err := validateTokenLookup(ctx, op, tokenLookup); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
available, err := client.capabilities(ctx, requiredCapabilities.paths())
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault capabilities"))
|
|
}
|
|
missing := available.missing(requiredCapabilities)
|
|
if len(missing) > 0 {
|
|
return nil,
|
|
errors.New(ctx, errors.VaultTokenMissingCapabilities, op, fmt.Sprintf("missing capabilities: %v", missing))
|
|
}
|
|
|
|
renewedToken, err := client.renewToken(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to renew vault token"))
|
|
}
|
|
|
|
tokenExpires, err := renewedToken.TokenTTL()
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault token expiration"))
|
|
}
|
|
|
|
accessor, err := renewedToken.TokenAccessor()
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault token accessor"))
|
|
}
|
|
|
|
token, err := newToken(ctx, id, cs.inputToken, []byte(accessor), tokenExpires)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
runJobsInterval := r.scheduler.GetRunJobsInterval()
|
|
if token.expiration <= runJobsInterval {
|
|
return nil, errors.Wrap(ctx, fmt.Errorf("scheduler interval must be greater than token ttl. scheduler jobs interval value: %s", runJobsInterval.String()), op)
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, cs.ProjectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, cs.ProjectId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
|
|
// encrypt
|
|
if err := token.encrypt(ctx, databaseWrapper); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if cs.clientCert != nil {
|
|
if err := cs.clientCert.encrypt(ctx, databaseWrapper); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var newToken *Token
|
|
var newClientCertificate *ClientCertificate
|
|
var newCredentialStore *CredentialStore
|
|
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(_ db.Reader, w db.Writer) error {
|
|
msgs := make([]*oplog.Message, 0, 3)
|
|
ticket, err := w.GetTicket(ctx, cs)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
// insert credential store
|
|
newCredentialStore = cs.clone()
|
|
var csOplogMsg oplog.Message
|
|
if err := w.Create(ctx, newCredentialStore, db.NewOplogMsg(&csOplogMsg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
msgs = append(msgs, &csOplogMsg)
|
|
|
|
// insert token
|
|
newToken = token.clone()
|
|
query, values := newToken.insertQuery()
|
|
rows, err := w.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 token would have been created")
|
|
}
|
|
msgs = append(msgs, newToken.oplogMessage(db.CreateOp))
|
|
|
|
newCredentialStore.inputToken = nil
|
|
newToken.Token.Token = nil
|
|
newToken.Token.CtToken = nil
|
|
newCredentialStore.outputToken = newToken
|
|
|
|
// insert client certificate (if exists)
|
|
if cs.clientCert != nil {
|
|
newClientCertificate = cs.clientCert.clone()
|
|
var clientCertOplogMsg oplog.Message
|
|
if err := w.Create(ctx, newClientCertificate, db.NewOplogMsg(&clientCertOplogMsg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
msgs = append(msgs, &clientCertOplogMsg)
|
|
|
|
newClientCertificate.CertificateKey = nil
|
|
newClientCertificate.CtCertificateKey = nil
|
|
newCredentialStore.clientCert = newClientCertificate
|
|
|
|
}
|
|
metadata := cs.oplog(oplog.OpType_OP_TYPE_CREATE)
|
|
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in project: %s: name %s already exists", cs.ProjectId, cs.Name)))
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in project: %s", cs.ProjectId)))
|
|
}
|
|
|
|
// Best effort update next run time of token renewal job, but an error should not
|
|
// cause update to fail.
|
|
// TODO (lcr 05/2021): log error once repo has logger
|
|
_ = r.scheduler.UpdateJobNextRunInAtLeast(ctx, tokenRenewalJobName, token.renewalIn())
|
|
|
|
return newCredentialStore, nil
|
|
}
|
|
|
|
func validateTokenLookup(ctx context.Context, op errors.Op, s *vault.Secret) error {
|
|
if s.Data == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "vault secret is not a token lookup")
|
|
}
|
|
|
|
if s.Data["renewable"] == nil {
|
|
return errors.E(ctx, errors.WithCode(errors.VaultTokenNotRenewable), errors.WithOp(op))
|
|
}
|
|
renewable, err := parseutil.ParseBool(s.Data["renewable"])
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if !renewable {
|
|
return errors.E(ctx, errors.WithCode(errors.VaultTokenNotRenewable), errors.WithOp(op))
|
|
}
|
|
|
|
if s.Data["orphan"] == nil {
|
|
return errors.E(ctx, errors.WithCode(errors.VaultTokenNotOrphan), errors.WithOp(op))
|
|
}
|
|
orphan, err := parseutil.ParseBool(s.Data["orphan"])
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if !orphan {
|
|
return errors.E(ctx, errors.WithCode(errors.VaultTokenNotOrphan), errors.WithOp(op))
|
|
}
|
|
|
|
if s.Data["period"] == nil {
|
|
return errors.E(ctx, errors.WithCode(errors.VaultTokenNotPeriodic), errors.WithOp(op))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LookupCredentialStore returns the CredentialStore for publicId. Returns
|
|
// nil, nil if no CredentialStore is found for publicId.
|
|
func (r *Repository) LookupCredentialStore(ctx context.Context, publicId string, _ ...Option) (*CredentialStore, error) {
|
|
const op = "vault.(Repository).LookupCredentialStore"
|
|
if publicId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
|
|
}
|
|
agg := allocListLookupStore()
|
|
agg.PublicId = publicId
|
|
if err := r.reader.LookupByPublicId(ctx, agg); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId)))
|
|
}
|
|
return agg.toCredentialStore(), nil
|
|
}
|
|
|
|
type listLookupStore struct {
|
|
PublicId string `gorm:"primary_key"`
|
|
ProjectId string
|
|
Name string
|
|
Description string
|
|
CreateTime *timestamp.Timestamp
|
|
UpdateTime *timestamp.Timestamp
|
|
Version uint32
|
|
VaultAddress string
|
|
Namespace string
|
|
CaCert []byte
|
|
TlsServerName string
|
|
TlsSkipVerify bool
|
|
WorkerFilter string
|
|
TokenHmac []byte
|
|
TokenStatus string
|
|
ClientCert []byte
|
|
ClientCertKeyHmac []byte
|
|
}
|
|
|
|
func allocListLookupStore() *listLookupStore {
|
|
return &listLookupStore{}
|
|
}
|
|
|
|
func (ps *listLookupStore) toCredentialStore() *CredentialStore {
|
|
cs := allocCredentialStore()
|
|
cs.PublicId = ps.PublicId
|
|
cs.ProjectId = ps.ProjectId
|
|
cs.Name = ps.Name
|
|
cs.Description = ps.Description
|
|
cs.CreateTime = ps.CreateTime
|
|
cs.UpdateTime = ps.UpdateTime
|
|
cs.Version = ps.Version
|
|
cs.VaultAddress = ps.VaultAddress
|
|
cs.Namespace = ps.Namespace
|
|
cs.CaCert = ps.CaCert
|
|
cs.TlsServerName = ps.TlsServerName
|
|
cs.TlsSkipVerify = ps.TlsSkipVerify
|
|
cs.WorkerFilter = ps.WorkerFilter
|
|
|
|
tk := allocToken()
|
|
tk.TokenHmac = ps.TokenHmac
|
|
tk.Status = ps.TokenStatus
|
|
cs.outputToken = tk
|
|
|
|
if ps.ClientCert != nil {
|
|
cert := allocClientCertificate()
|
|
cert.Certificate = ps.ClientCert
|
|
cert.CertificateKeyHmac = ps.ClientCertKeyHmac
|
|
cs.clientCert = cert
|
|
}
|
|
return cs
|
|
}
|
|
|
|
// TableName returns the table name for gorm.
|
|
func (*listLookupStore) TableName() string { return "credential_vault_store_list_lookup" }
|
|
|
|
// GetPublicId returns the public id.
|
|
func (ps *listLookupStore) GetPublicId() string { return ps.PublicId }
|
|
|
|
// UpdateCredentialStore updates the repository entry for cs.PublicId with
|
|
// the values in cs for the fields listed in fieldMaskPaths. It returns a
|
|
// new CredentialStore containing the updated values and a count of the
|
|
// number of records updated. cs is not changed.
|
|
//
|
|
// cs must contain a valid PublicId. Only Name, Description, Namespace,
|
|
// TlsServerName, TlsSkipVerify, CaCert, VaultAddress, ClientCertificate,
|
|
// ClientCertificateKey, workerFilter, and Token can be changed. If cs.Name is set to a
|
|
// non-empty string, it must be unique within cs.Projectid. If Token is changed,
|
|
// the new token must have the same properties defined in CreateCredentialStore
|
|
// and UpdateCredentialStore calls the same Vault endpoints described in
|
|
// CreateCredentialStore.
|
|
//
|
|
// An attribute of cs will be set to NULL in the database if the attribute
|
|
// in cs is the zero value and it is included in fieldMaskPaths.
|
|
func (r *Repository) UpdateCredentialStore(ctx context.Context, cs *CredentialStore, version uint32, fieldMaskPaths []string, _ ...Option) (*CredentialStore, int, error) {
|
|
const op = "vault.(Repository).UpdateCredentialStore"
|
|
if cs == nil {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing CredentialStore")
|
|
}
|
|
if cs.CredentialStore == nil {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded CredentialStore")
|
|
}
|
|
if cs.PublicId == "" {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
|
}
|
|
if version == 0 {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
|
|
}
|
|
if cs.ProjectId == "" {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id")
|
|
}
|
|
cs = cs.clone()
|
|
|
|
var validateToken, updateToken bool
|
|
for _, f := range fieldMaskPaths {
|
|
switch {
|
|
case strings.EqualFold(nameField, f):
|
|
case strings.EqualFold(descriptionField, f):
|
|
case strings.EqualFold(namespaceField, f):
|
|
case strings.EqualFold(tlsServerNameField, f):
|
|
case strings.EqualFold(tlsSkipVerifyField, f):
|
|
case strings.EqualFold(workerFilterField, f):
|
|
case strings.EqualFold(caCertField, f):
|
|
case strings.EqualFold(vaultAddressField, f):
|
|
validateToken = true
|
|
case strings.EqualFold(certificateField, f):
|
|
case strings.EqualFold(certificateKeyField, f):
|
|
case strings.EqualFold(tokenField, f):
|
|
if len(cs.inputToken) != 0 {
|
|
updateToken = true
|
|
validateToken = true
|
|
}
|
|
default:
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
|
|
}
|
|
}
|
|
dbMask, nullFields := dbw.BuildUpdatePaths(
|
|
map[string]any{
|
|
nameField: cs.Name,
|
|
descriptionField: cs.Description,
|
|
namespaceField: cs.Namespace,
|
|
tlsServerNameField: cs.TlsServerName,
|
|
tlsSkipVerifyField: cs.TlsSkipVerify,
|
|
workerFilterField: cs.WorkerFilter,
|
|
caCertField: cs.CaCert,
|
|
vaultAddressField: cs.VaultAddress,
|
|
tokenField: cs.inputToken,
|
|
},
|
|
fieldMaskPaths,
|
|
[]string{
|
|
tlsSkipVerifyField,
|
|
},
|
|
)
|
|
var clientCert, clientCertKey []byte
|
|
if cs.ClientCertificate() != nil {
|
|
clientCert = cs.ClientCertificate().GetCertificate()
|
|
clientCertKey = cs.ClientCertificate().GetCertificateKey()
|
|
}
|
|
certDbMask, certNullFields := dbw.BuildUpdatePaths(
|
|
map[string]any{
|
|
certificateField: clientCert,
|
|
certificateKeyField: clientCertKey,
|
|
},
|
|
fieldMaskPaths, nil,
|
|
)
|
|
if len(certNullFields) != 0 && len(certNullFields) != 2 {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "attempting to unset a required field on a client cert")
|
|
}
|
|
if len(append(dbMask, certDbMask...)) == 0 && len(append(nullFields, certNullFields...)) == 0 {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask")
|
|
}
|
|
|
|
var filteredDbMask, filteredNullFields []string
|
|
for _, f := range dbMask {
|
|
switch {
|
|
case strings.EqualFold(tokenField, f):
|
|
default:
|
|
filteredDbMask = append(filteredDbMask, f)
|
|
}
|
|
}
|
|
for _, f := range nullFields {
|
|
switch {
|
|
case strings.EqualFold(tokenField, f):
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "attempting to unset the value for token")
|
|
default:
|
|
filteredNullFields = append(filteredNullFields, f)
|
|
}
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, cs.ProjectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected,
|
|
errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, cs.ProjectId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected,
|
|
errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
|
|
ps, err := r.lookupClientStore(ctx, cs.GetPublicId())
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup private credential store"))
|
|
}
|
|
if ps == nil {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("credential store %s", cs.PublicId))
|
|
}
|
|
origStore := ps.toCredentialStore()
|
|
origStore.inputToken = ps.Token
|
|
if len(ps.ClientCert) > 0 {
|
|
origStore.clientCert, err = NewClientCertificate(ctx, ps.ClientCert, ps.ClientKey)
|
|
}
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("can't recreate client certificate for vault client creation"))
|
|
}
|
|
updatedStore := origStore.applyUpdate(cs, fieldMaskPaths)
|
|
|
|
if len(certDbMask) > 0 && updatedStore.clientCert != nil {
|
|
if err := updatedStore.clientCert.encrypt(ctx, databaseWrapper); err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var token *Token
|
|
client, err := updatedStore.client(ctx)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get client for updated store"))
|
|
}
|
|
if validateToken {
|
|
tokenLookup, err := client.lookupToken(ctx)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("cannot lookup token for updated store"))
|
|
}
|
|
if err := validateTokenLookup(ctx, op, tokenLookup); err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
available, err := client.capabilities(ctx, requiredCapabilities.paths())
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault capabilities"))
|
|
}
|
|
missing := available.missing(requiredCapabilities)
|
|
if len(missing) > 0 {
|
|
return nil,
|
|
db.NoRowsAffected,
|
|
errors.New(ctx, errors.VaultTokenMissingCapabilities, op, fmt.Sprintf("missing capabilities: %v", missing))
|
|
}
|
|
}
|
|
if updateToken {
|
|
renewedToken, err := client.renewToken(ctx)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to renew vault token"))
|
|
}
|
|
tokenExpires, err := renewedToken.TokenTTL()
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault token expiration"))
|
|
}
|
|
accessor, err := renewedToken.TokenAccessor()
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault token accessor"))
|
|
}
|
|
if token, err = newToken(ctx, cs.GetPublicId(), cs.inputToken, []byte(accessor), tokenExpires); err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
}
|
|
runJobsInterval := r.scheduler.GetRunJobsInterval()
|
|
if token.expiration <= runJobsInterval {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, fmt.Errorf("scheduler interval must be greater than token ttl. scheduler jobs interval value: %s", runJobsInterval.String()), op)
|
|
}
|
|
// encrypt token
|
|
if err := token.encrypt(ctx, databaseWrapper); err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var rowsUpdated int
|
|
var returnedCredentialStore *CredentialStore
|
|
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(reader db.Reader, w db.Writer) error {
|
|
msgs := make([]*oplog.Message, 0, 3)
|
|
ticket, err := w.GetTicket(ctx, cs)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
cs := cs.clone()
|
|
var csOplogMsg oplog.Message
|
|
|
|
switch {
|
|
case len(filteredDbMask) == 0 && len(filteredNullFields) == 0:
|
|
// the credential store's fields are not being updated,
|
|
// just it's token or client certificate, so we need to
|
|
// just update the credential store's version.
|
|
cs.Version = version + 1
|
|
rowsUpdated, err = w.Update(ctx, cs, []string{"Version"}, nil, db.NewOplogMsg(&csOplogMsg), db.WithVersion(&version))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential store version"))
|
|
}
|
|
switch rowsUpdated {
|
|
case 1:
|
|
case 0:
|
|
return nil
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential store version and %d rows updated", rowsUpdated))
|
|
}
|
|
default:
|
|
rowsUpdated, err = w.Update(ctx, cs, filteredDbMask, filteredNullFields, db.NewOplogMsg(&csOplogMsg), db.WithVersion(&version))
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return errors.New(ctx, errors.NotUnique, op,
|
|
fmt.Sprintf("name %s already exists: %s", cs.Name, cs.PublicId))
|
|
}
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential store"))
|
|
}
|
|
switch rowsUpdated {
|
|
case 1:
|
|
case 0:
|
|
return nil
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential store and %d rows updated", rowsUpdated))
|
|
}
|
|
}
|
|
msgs = append(msgs, &csOplogMsg)
|
|
|
|
switch {
|
|
case len(certNullFields) == 2:
|
|
// Delete the certificate
|
|
deleteCert := allocClientCertificate()
|
|
deleteCert.StoreId = cs.GetPublicId()
|
|
query, values := deleteCert.deleteQuery()
|
|
rows, err := w.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete client certificate"))
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 client certificate would have been deleted")
|
|
}
|
|
msgs = append(msgs, deleteCert.oplogMessage(db.DeleteOp))
|
|
case len(certDbMask) > 0:
|
|
if updatedStore.clientCert == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "updated cert")
|
|
}
|
|
query, values := updatedStore.clientCert.insertQuery()
|
|
rows, err := w.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to upsert client certificate"))
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 client certificate would have been upserted")
|
|
}
|
|
}
|
|
|
|
if updateToken {
|
|
query, values := token.insertQuery()
|
|
rows, err := w.Exec(ctx, query, values)
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return errors.New(ctx, errors.NotUnique, op,
|
|
fmt.Sprintf("token already exists"))
|
|
}
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 token would have been created")
|
|
}
|
|
msgs = append(msgs, token.oplogMessage(db.CreateOp))
|
|
}
|
|
|
|
publicId := cs.PublicId
|
|
agg := allocListLookupStore()
|
|
agg.PublicId = publicId
|
|
if err := reader.LookupByPublicId(ctx, agg); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to lookup credential store: %s", publicId)))
|
|
}
|
|
returnedCredentialStore = agg.toCredentialStore()
|
|
|
|
metadata := cs.oplog(oplog.OpType_OP_TYPE_UPDATE)
|
|
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, err
|
|
}
|
|
|
|
if updateToken && token != nil {
|
|
// Best effort update next run time of token renewal job, but an error should not
|
|
// cause update to fail.
|
|
// TODO (lcr 05/2021): log error once repo has logger
|
|
_ = r.scheduler.UpdateJobNextRunInAtLeast(ctx, tokenRenewalJobName, token.renewalIn())
|
|
}
|
|
|
|
return returnedCredentialStore, rowsUpdated, nil
|
|
}
|
|
|
|
// DeleteCredentialStore deletes publicId from the repository and returns
|
|
// the number of records deleted. All options are ignored.
|
|
func (r *Repository) DeleteCredentialStore(ctx context.Context, publicId string, _ ...Option) (int, error) {
|
|
const op = "vault.(Repository).DeleteCredentialStore"
|
|
if publicId == "" {
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id")
|
|
}
|
|
|
|
cs := allocCredentialStore()
|
|
cs.PublicId = publicId
|
|
if err := r.reader.LookupByPublicId(ctx, cs); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return db.NoRowsAffected, nil
|
|
}
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId)))
|
|
}
|
|
if cs.ProjectId == "" {
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id")
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, cs.ProjectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
var rows int
|
|
query, values := cs.softDeleteQuery()
|
|
_, err = r.writer.DoTx(
|
|
ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(_ db.Reader, w db.Writer) (err error) {
|
|
var msgs []*oplog.Message
|
|
ticket, err := w.GetTicket(ctx, cs)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
rows, err = w.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 credential store would have been deleted")
|
|
}
|
|
msg := cs.oplogMessage(db.UpdateOp)
|
|
msg.FieldMaskPaths = []string{"DeleteTime"}
|
|
msgs = append(msgs, msg)
|
|
|
|
metadata := cs.oplog(oplog.OpType_OP_TYPE_UPDATE)
|
|
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", cs.PublicId)))
|
|
}
|
|
|
|
if rows > 0 {
|
|
// Schedule token revocation and credential store cleanup jobs to run immediately
|
|
_ = r.scheduler.UpdateJobNextRunInAtLeast(ctx, tokenRevocationJobName, 0, scheduler.WithRunNow(true))
|
|
_ = r.scheduler.UpdateJobNextRunInAtLeast(ctx, credentialStoreCleanupJobName, 0, scheduler.WithRunNow(true))
|
|
}
|
|
return rows, nil
|
|
}
|