* fix (auth): properly handle rows.Err() * fix (auth/ldap): properly handle rows.Err() * fix (auth/oidc): properly handle rows.Err() * fix (auth/password): properly handle rows.Err() * fix (authtoken): properly handle rows.Err() * fix (credential): properly handle rows.Err() * fix (credential/static): properly handle rows.Err() * fix (credential/vault): properly handle rows.Err() * fix (db): properly handle rows.Err() * fix (db/schema): properly handle rows.Err() * fix (db/migrations): properly handle rows.Err() * fix (host): properly handle rows.Err() * fix (host/plugin): properly handle rows.Err() * fix (host/static): properly handle rows.Err() * fix (iam): properly handle rows.Err() * fix (kms): properly handle rows.Err() * fix (oplog): properly handle rows.Err() * fix (pagination/purge): properly handle rows.Err() * fix (plugin): properly handle rows.Err() * fix (job/scheduler): properly handle rows.Err() * fix (server): properly handle rows.Err() * fix (session): properly handle rows.Err() * fix (target): properly handle rows.Err()
635 lines
23 KiB
Go
635 lines
23 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/credential"
|
|
"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/go-dbw"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
)
|
|
|
|
// CreateCredentialLibrary inserts l into the repository and returns a new
|
|
// CredentialLibrary containing the credential library's PublicId. l is not
|
|
// changed. l must contain a valid StoreId. l must not contain a PublicId.
|
|
// The PublicId is generated and assigned by this method.
|
|
//
|
|
// Both l.Name and l.Description are optional. If l.Name is set, it must be
|
|
// unique within l.StoreId.
|
|
//
|
|
// Both l.CreateTime and l.UpdateTime are ignored.
|
|
func (r *Repository) CreateCredentialLibrary(ctx context.Context, projectId string, l *CredentialLibrary, _ ...Option) (*CredentialLibrary, error) {
|
|
const op = "vault.(Repository).CreateCredentialLibrary"
|
|
if l == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil CredentialLibrary")
|
|
}
|
|
if l.CredentialLibrary == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded l")
|
|
}
|
|
if l.StoreId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no store id")
|
|
}
|
|
if l.VaultPath == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no vault path")
|
|
}
|
|
if l.PublicId != "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty")
|
|
}
|
|
if projectId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id")
|
|
}
|
|
l = l.clone()
|
|
|
|
if l.HttpMethod == "" {
|
|
l.HttpMethod = string(MethodGet)
|
|
}
|
|
|
|
if err := l.validate(ctx, op); err != nil {
|
|
return nil, err // intentionally not wrapped.
|
|
}
|
|
|
|
id, err := newCredentialLibraryId(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
l.setId(id)
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
var newCredentialLibrary *CredentialLibrary
|
|
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(_ db.Reader, w db.Writer) error {
|
|
var msgs []*oplog.Message
|
|
ticket, err := w.GetTicket(ctx, l)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
// insert credential library
|
|
newCredentialLibrary = l.clone()
|
|
var lOplogMsg oplog.Message
|
|
if err := w.Create(ctx, newCredentialLibrary, db.NewOplogMsg(&lOplogMsg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
msgs = append(msgs, &lOplogMsg)
|
|
|
|
// insert mapper override (if exists)
|
|
if l.MappingOverride != nil {
|
|
var msg oplog.Message
|
|
if err := w.Create(ctx, newCredentialLibrary.MappingOverride, db.NewOplogMsg(&msg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
newCredentialLibrary.MappingOverride.sanitize()
|
|
msgs = append(msgs, &msg)
|
|
}
|
|
|
|
metadata := l.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 credential store: %s: name %s already exists", l.StoreId, l.Name)))
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in credential store: %s", l.StoreId)))
|
|
}
|
|
return newCredentialLibrary, nil
|
|
}
|
|
|
|
// UpdateCredentialLibrary updates the repository entry for l.PublicId with
|
|
// the values in l for the fields listed in fieldMaskPaths. It returns a
|
|
// new CredentialLibrary containing the updated values and a count of the
|
|
// number of records updated. l is not changed.
|
|
//
|
|
// l must contain a valid PublicId. Only Name, Description, VaultPath,
|
|
// HttpMethod, HttpRequestBody, and MappingOverride can be updated. If
|
|
// l.Name is set to a non-empty string, it must be unique within l.StoreId.
|
|
//
|
|
// An attribute of l will be set to NULL in the database if the attribute
|
|
// in l is the zero value and it is included in fieldMaskPaths except for
|
|
// HttpMethod. If HttpMethod is in the fieldMaskPath but l.HttpMethod
|
|
// is not set it will be set to the value "GET". If storage has a value
|
|
// for HttpRequestBody when l.HttpMethod is set to GET the update will fail.
|
|
func (r *Repository) UpdateCredentialLibrary(ctx context.Context, projectId string, l *CredentialLibrary, version uint32, fieldMaskPaths []string, _ ...Option) (*CredentialLibrary, int, error) {
|
|
const op = "vault.(Repository).UpdateCredentialLibrary"
|
|
if l == nil {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing CredentialLibrary")
|
|
}
|
|
if l.CredentialLibrary == nil {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded CredentialLibrary")
|
|
}
|
|
if l.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 projectId == "" {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id")
|
|
}
|
|
l = l.clone()
|
|
|
|
var updateMappingOverride bool
|
|
for _, f := range fieldMaskPaths {
|
|
switch {
|
|
case strings.EqualFold(nameField, f):
|
|
case strings.EqualFold(descriptionField, f):
|
|
case strings.EqualFold(vaultPathField, f):
|
|
case strings.EqualFold(httpMethodField, f):
|
|
case strings.EqualFold(httpRequestBodyField, f):
|
|
case strings.EqualFold(MappingOverrideField, f):
|
|
updateMappingOverride = true
|
|
default:
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
|
|
}
|
|
}
|
|
var dbMask, nullFields []string
|
|
dbMask, nullFields = dbw.BuildUpdatePaths(
|
|
map[string]any{
|
|
nameField: l.Name,
|
|
descriptionField: l.Description,
|
|
vaultPathField: l.VaultPath,
|
|
httpMethodField: l.HttpMethod,
|
|
httpRequestBodyField: l.HttpRequestBody,
|
|
MappingOverrideField: l.MappingOverride,
|
|
},
|
|
fieldMaskPaths,
|
|
nil,
|
|
)
|
|
|
|
if strutil.StrListContains(nullFields, httpMethodField) {
|
|
// GET is the default value for the HttpMethod field in
|
|
// CredentialLibrary. The http_method column in the database does
|
|
// not allow NULL values but it also does not define a default
|
|
// value. Therefore, if the httpMethodField is in nullFields:
|
|
// remove it from nullFields then add it to dbMask and set the
|
|
// value to GET.
|
|
dbMask = append(dbMask, httpMethodField)
|
|
nullFields = strutil.StrListDelete(nullFields, httpMethodField)
|
|
l.HttpMethod = string(MethodGet)
|
|
}
|
|
|
|
if len(dbMask) == 0 && len(nullFields) == 0 {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask")
|
|
}
|
|
|
|
origLib, err := r.LookupCredentialLibrary(ctx, l.PublicId)
|
|
switch {
|
|
case err != nil:
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
case origLib == nil:
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("credential library %s", l.PublicId))
|
|
case updateMappingOverride && !validMappingOverride(l.MappingOverride, origLib.CredentialType()):
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.VaultInvalidMappingOverride, op, "invalid mapping override for credential type")
|
|
}
|
|
|
|
var filteredDbMask, filteredNullFields []string
|
|
for _, f := range dbMask {
|
|
switch {
|
|
case strings.EqualFold(MappingOverrideField, f):
|
|
default:
|
|
filteredDbMask = append(filteredDbMask, f)
|
|
}
|
|
}
|
|
for _, f := range nullFields {
|
|
switch {
|
|
case strings.EqualFold(MappingOverrideField, f):
|
|
default:
|
|
filteredNullFields = append(filteredNullFields, f)
|
|
}
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt),
|
|
errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
var rowsUpdated int
|
|
var returnedCredentialLibrary *CredentialLibrary
|
|
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(rr db.Reader, w db.Writer) error {
|
|
var msgs []*oplog.Message
|
|
ticket, err := w.GetTicket(ctx, l)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
l := l.clone()
|
|
var lOplogMsg oplog.Message
|
|
|
|
// Update the credential library table
|
|
switch {
|
|
case len(filteredDbMask) == 0 && len(filteredNullFields) == 0:
|
|
// the credential library's fields are not being updated,
|
|
// just one of it's child objects, so we just need to
|
|
// update the library's version.
|
|
l.Version = version + 1
|
|
rowsUpdated, err = w.Update(ctx, l, []string{"Version"}, nil, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library version"))
|
|
}
|
|
switch rowsUpdated {
|
|
case 1:
|
|
case 0:
|
|
return nil
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library version and %d rows updated", rowsUpdated))
|
|
}
|
|
default:
|
|
rowsUpdated, err = w.Update(ctx, l, filteredDbMask, filteredNullFields, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version))
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return errors.New(ctx, errors.NotUnique, op,
|
|
fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId))
|
|
}
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library"))
|
|
}
|
|
switch rowsUpdated {
|
|
case 1:
|
|
case 0:
|
|
return nil
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library and %d rows updated", rowsUpdated))
|
|
}
|
|
}
|
|
msgs = append(msgs, &lOplogMsg)
|
|
|
|
// Update a credential mapping override table if applicable
|
|
if updateMappingOverride {
|
|
// delete the current mapping override if it exists
|
|
if origLib.MappingOverride != nil {
|
|
var msg oplog.Message
|
|
rowsDeleted, err := w.Delete(ctx, origLib.MappingOverride, db.NewOplogMsg(&msg))
|
|
switch {
|
|
case err != nil:
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete mapping override"))
|
|
default:
|
|
switch rowsDeleted {
|
|
case 0, 1:
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("delete mapping override and %d rows deleted", rowsDeleted))
|
|
}
|
|
}
|
|
msgs = append(msgs, &msg)
|
|
}
|
|
// insert a new mapping override if specified
|
|
if l.MappingOverride != nil {
|
|
var msg oplog.Message
|
|
l.MappingOverride.setLibraryId(l.PublicId)
|
|
if err := w.Create(ctx, l.MappingOverride, db.NewOplogMsg(&msg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
msgs = append(msgs, &msg)
|
|
}
|
|
}
|
|
|
|
metadata := l.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"))
|
|
}
|
|
|
|
pl := allocListLookupLibrary()
|
|
pl.PublicId = l.PublicId
|
|
if err := rr.LookupByPublicId(ctx, pl); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library"))
|
|
}
|
|
returnedCredentialLibrary = pl.toCredentialLibrary()
|
|
return nil
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op,
|
|
fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId))
|
|
}
|
|
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(l.PublicId))
|
|
}
|
|
|
|
return returnedCredentialLibrary, rowsUpdated, nil
|
|
}
|
|
|
|
// LookupCredentialLibrary returns the CredentialLibrary for publicId.
|
|
// Returns nil, nil if no CredentialLibrary is found for publicId.
|
|
func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId string, _ ...Option) (*CredentialLibrary, error) {
|
|
const op = "vault.(Repository).LookupCredentialLibrary"
|
|
if publicId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
|
|
}
|
|
l := allocListLookupLibrary()
|
|
l.PublicId = publicId
|
|
if err := r.reader.LookupByPublicId(ctx, l); 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 l.toCredentialLibrary(), nil
|
|
}
|
|
|
|
// listLookupLibrary is a credential library and any of library's credential
|
|
// mapping overrides. It does not include encrypted data and is safe to
|
|
// return external to boundary.
|
|
type listLookupLibrary struct {
|
|
PublicId string `gorm:"primary_key"`
|
|
StoreId string
|
|
Name string
|
|
Description string
|
|
CreateTime *timestamp.Timestamp
|
|
UpdateTime *timestamp.Timestamp
|
|
Version uint32
|
|
VaultPath string
|
|
HttpMethod string
|
|
HttpRequestBody []byte
|
|
CredentialType string
|
|
UsernameAttribute string
|
|
PasswordAttribute string
|
|
PrivateKeyAttribute string
|
|
PrivateKeyPassphraseAttribute string
|
|
}
|
|
|
|
func allocListLookupLibrary() *listLookupLibrary {
|
|
return &listLookupLibrary{}
|
|
}
|
|
|
|
func (pl *listLookupLibrary) toCredentialLibrary() *CredentialLibrary {
|
|
cl := allocCredentialLibrary()
|
|
cl.PublicId = pl.PublicId
|
|
cl.StoreId = pl.StoreId
|
|
cl.Name = pl.Name
|
|
cl.Description = pl.Description
|
|
cl.CreateTime = pl.CreateTime
|
|
cl.UpdateTime = pl.UpdateTime
|
|
cl.Version = pl.Version
|
|
cl.VaultPath = pl.VaultPath
|
|
cl.HttpMethod = pl.HttpMethod
|
|
cl.HttpRequestBody = pl.HttpRequestBody
|
|
cl.CredentialLibrary.CredentialType = pl.CredentialType
|
|
|
|
switch pl.CredentialType {
|
|
case string(globals.UsernamePasswordCredentialType):
|
|
if pl.UsernameAttribute != "" || pl.PasswordAttribute != "" {
|
|
up := allocUsernamePasswordOverride()
|
|
up.LibraryId = pl.PublicId
|
|
up.UsernameAttribute = pl.UsernameAttribute
|
|
up.PasswordAttribute = pl.PasswordAttribute
|
|
up.sanitize()
|
|
cl.MappingOverride = up
|
|
}
|
|
case string(globals.SshPrivateKeyCredentialType):
|
|
if pl.UsernameAttribute != "" || pl.PrivateKeyAttribute != "" || pl.PrivateKeyPassphraseAttribute != "" {
|
|
pk := allocSshPrivateKeyOverride()
|
|
pk.LibraryId = pl.PublicId
|
|
pk.UsernameAttribute = pl.UsernameAttribute
|
|
pk.PrivateKeyAttribute = pl.PrivateKeyAttribute
|
|
pk.PrivateKeyPassphraseAttribute = pl.PrivateKeyPassphraseAttribute
|
|
pk.sanitize()
|
|
cl.MappingOverride = pk
|
|
}
|
|
}
|
|
return cl
|
|
}
|
|
|
|
// TableName returns the table name for gorm.
|
|
func (*listLookupLibrary) TableName() string { return "credential_vault_library_list_lookup" }
|
|
|
|
// GetPublicId returns the public id.
|
|
func (pl *listLookupLibrary) GetPublicId() string { return pl.PublicId }
|
|
|
|
// DeleteCredentialLibrary deletes publicId from the repository and returns
|
|
// the number of records deleted.
|
|
func (r *Repository) DeleteCredentialLibrary(ctx context.Context, projectId string, publicId string, _ ...Option) (int, error) {
|
|
const op = "vault.(Repository).DeleteCredentialLibrary"
|
|
if publicId == "" {
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id")
|
|
}
|
|
if projectId == "" {
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id")
|
|
}
|
|
|
|
l := allocCredentialLibrary()
|
|
l.PublicId = publicId
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
var rowsDeleted int
|
|
_, err = r.writer.DoTx(
|
|
ctx, db.StdRetryCnt, db.ExpBackoff{},
|
|
func(_ db.Reader, w db.Writer) (err error) {
|
|
dl := l.clone()
|
|
rowsDeleted, err = w.Delete(ctx, dl, db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_DELETE)))
|
|
if err == nil && rowsDeleted > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 CredentialLibrary would have been deleted")
|
|
}
|
|
return err
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", l.PublicId)))
|
|
}
|
|
|
|
return rowsDeleted, nil
|
|
}
|
|
|
|
// ListLibraries returns a slice of CredentialLibraries for the
|
|
// storeId. Supports the following options:
|
|
// - credential.WithLimit
|
|
// - credential.WithStartPageAfterItem
|
|
func (r *Repository) ListLibraries(ctx context.Context, storeId string, opt ...credential.Option) ([]credential.Library, time.Time, error) {
|
|
const op = "vault.(Repository).ListLibraries"
|
|
if storeId == "" {
|
|
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing store id")
|
|
}
|
|
opts, err := credential.GetOpts(opt...)
|
|
if err != nil {
|
|
return nil, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
limit := r.defaultLimit
|
|
if opts.WithLimit != 0 {
|
|
// non-zero signals an override of the default limit for the repo.
|
|
limit = opts.WithLimit
|
|
}
|
|
query := fmt.Sprintf(listLibrariesTemplate, limit)
|
|
args := []any{sql.Named("store_id", storeId)}
|
|
if opts.WithStartPageAfterItem != nil {
|
|
query = fmt.Sprintf(listLibrariesPageTemplate, limit)
|
|
args = append(args,
|
|
sql.Named("last_item_create_time", opts.WithStartPageAfterItem.GetCreateTime()),
|
|
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
|
|
)
|
|
}
|
|
|
|
libs, transactionTimestamp, err := r.queryLibraries(ctx, query, args)
|
|
if err != nil {
|
|
return nil, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Sort final slice to ensure correct ordering.
|
|
// We sort by create time descending (most recently created first).
|
|
slices.SortFunc(libs, func(i, j credential.Library) int {
|
|
return j.GetCreateTime().AsTime().Compare(i.GetCreateTime().AsTime())
|
|
})
|
|
|
|
return libs, transactionTimestamp, nil
|
|
}
|
|
|
|
// ListLibrariesRefresh returns a slice of credential libraries
|
|
// for the store ID. Supports the following options:
|
|
// - credential.WithLimit
|
|
// - credential.WithStartPageAfterItem
|
|
func (r *Repository) ListLibrariesRefresh(ctx context.Context, storeId string, updatedAfter time.Time, opt ...credential.Option) ([]credential.Library, time.Time, error) {
|
|
const op = "vault.(Repository).ListLibrariesRefresh"
|
|
switch {
|
|
case storeId == "":
|
|
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing credential store ID")
|
|
case updatedAfter.IsZero():
|
|
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
|
|
}
|
|
opts, err := credential.GetOpts(opt...)
|
|
if err != nil {
|
|
return nil, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
limit := r.defaultLimit
|
|
if opts.WithLimit != 0 {
|
|
// non-zero signals an override of the default limit for the repo.
|
|
limit = opts.WithLimit
|
|
}
|
|
|
|
query := fmt.Sprintf(listLibrariesRefreshTemplate, limit)
|
|
args := []any{
|
|
sql.Named("store_id", storeId),
|
|
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
|
|
}
|
|
if opts.WithStartPageAfterItem != nil {
|
|
query = fmt.Sprintf(listLibrariesRefreshPageTemplate, limit)
|
|
args = append(args,
|
|
sql.Named("last_item_update_time", opts.WithStartPageAfterItem.GetUpdateTime()),
|
|
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
|
|
)
|
|
}
|
|
|
|
libs, transactionTimestamp, err := r.queryLibraries(ctx, query, args)
|
|
if err != nil {
|
|
return nil, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Sort final slice to ensure correct ordering.
|
|
// We sort by update time descending (most recently updated first).
|
|
slices.SortFunc(libs, func(i, j credential.Library) int {
|
|
return j.GetUpdateTime().AsTime().Compare(i.GetUpdateTime().AsTime())
|
|
})
|
|
|
|
return libs, transactionTimestamp, nil
|
|
}
|
|
|
|
func (r *Repository) queryLibraries(ctx context.Context, query string, args []any) ([]credential.Library, time.Time, error) {
|
|
const op = "vault.(Repository).queryLibraries"
|
|
|
|
var libs []credential.Library
|
|
var transactionTimestamp time.Time
|
|
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
|
|
rows, err := rd.Query(ctx, query, args)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
var results []listCredentialLibraryResult
|
|
for rows.Next() {
|
|
if err := rd.ScanRows(ctx, rows, &results); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
for _, result := range results {
|
|
lib, err := result.toLibrary(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
libs = append(libs, lib)
|
|
}
|
|
transactionTimestamp, err = rd.Now(ctx)
|
|
return err
|
|
}); err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
|
|
return libs, transactionTimestamp, nil
|
|
}
|
|
|
|
// EstimatedLibraryCount returns an estimate of the number of Vault credential libraries
|
|
func (r *Repository) EstimatedLibraryCount(ctx context.Context) (int, error) {
|
|
const op = "vault.(Repository).EstimatedLibraryCount"
|
|
rows, err := r.reader.Query(ctx, estimateCountCredentialLibraries, nil)
|
|
if err != nil {
|
|
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total vault credential libraries"))
|
|
}
|
|
var count int
|
|
for rows.Next() {
|
|
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
|
|
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total vault credential libraries"))
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total vault credential libraries"))
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// ListDeletedLibraryIds lists the public IDs of any credential libraries deleted since the timestamp provided.
|
|
func (r *Repository) ListDeletedLibraryIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
|
|
const op = "vault.(Repository).ListDeletedLibraryIds"
|
|
var credentialLibraryIds []string
|
|
var transactionTimestamp time.Time
|
|
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
|
var deletedCredentialLibraries []*deletedCredentialLibrary
|
|
if err := r.SearchWhere(ctx, &deletedCredentialLibraries, "delete_time >= ?", []any{since}); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted credential libraries"))
|
|
}
|
|
for _, cl := range deletedCredentialLibraries {
|
|
credentialLibraryIds = append(credentialLibraryIds, cl.PublicId)
|
|
}
|
|
var deletedSshCredentialLibraries []*deletedSSHCertificateCredentialLibrary
|
|
if err := r.SearchWhere(ctx, &deletedSshCredentialLibraries, "delete_time >= ?", []any{since}); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted SSH certificate credential libraries"))
|
|
}
|
|
for _, cl := range deletedSshCredentialLibraries {
|
|
credentialLibraryIds = append(credentialLibraryIds, cl.PublicId)
|
|
}
|
|
var err error
|
|
transactionTimestamp, err = r.Now(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query transaction timestamp"))
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
return credentialLibraryIds, transactionTimestamp, nil
|
|
}
|