Files
threshold/internal/credential/vault/repository_credential_store.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

741 lines
26 KiB
Go

// Copyright (c) HashiCorp, Inc.
// 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 capabilites: %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 capabilites: %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
}
// ListCredentialStores returns a slice of CredentialStores for the
// projectIds. WithLimit is the only option supported.
func (r *Repository) ListCredentialStores(ctx context.Context, projectIds []string, opt ...Option) ([]*CredentialStore, error) {
const op = "vault.(Repository).ListCredentialStores"
if len(projectIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no projectIds")
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var credentialStores []*listLookupStore
err := r.reader.SearchWhere(ctx, &credentialStores, "project_id in (?)", []any{projectIds}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var out []*CredentialStore
for _, ca := range credentialStores {
out = append(out, ca.toCredentialStore())
}
return out, 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
}