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.
boundary/internal/iam/repository.go

299 lines
9.4 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package iam
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
)
var ErrMetadataScopeNotFound = errors.New(context.Background(), errors.RecordNotFound, "iam", "scope not found for metadata", errors.WithoutEvent())
// Repository is the iam database repository
type Repository struct {
reader db.Reader
writer db.Writer
kms *kms.Kms
// defaultLimit provides a default for limiting the number of results returned from the repo
defaultLimit int
}
// NewRepository creates a new iam Repository. Supports the options: WithLimit
// which sets a default limit on results returned by repo operations.
func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
const op = "iam.NewRepository"
if r == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil reader")
}
if w == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil writer")
}
if kms == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil kms")
}
opts := getOpts(opt...)
if opts.withLimit == 0 {
// zero signals the boundary defaults should be used.
opts.withLimit = db.DefaultLimit
}
return &Repository{
reader: r,
writer: w,
kms: kms,
defaultLimit: opts.withLimit,
}, nil
}
// list will return a listing of resources and honor the WithLimit option or the
// repo defaultLimit
func (r *Repository) list(ctx context.Context, resources any, where string, args []any, opt ...Option) error {
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
}
return r.reader.SearchWhere(ctx, resources, where, args, db.WithLimit(limit))
}
// create will create a new iam resource in the db repository with an oplog entry
func (r *Repository) create(ctx context.Context, resource Resource, _ ...Option) (Resource, error) {
const op = "iam.(Repository).create"
if resource == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing resource")
}
resourceCloner, ok := resource.(Cloneable)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource is not Cloneable")
}
metadata, err := r.stdMetadata(ctx, resource)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error getting metadata"))
}
metadata["op-type"] = []string{oplog.OpType_OP_TYPE_CREATE.String()}
scope, err := resource.GetScope(ctx, r.reader)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get scope"))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var returnedResource any
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
returnedResource = resourceCloner.Clone()
err := w.Create(
ctx,
returnedResource,
db.WithOplog(oplogWrapper, metadata),
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return returnedResource.(Resource), nil
}
// update will update an iam resource in the db repository with an oplog entry
func (r *Repository) update(ctx context.Context, resource Resource, version uint32, fieldMaskPaths []string, setToNullPaths []string, opt ...Option) (Resource, int, error) {
const op = "iam.(Repository).update"
if version == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
if resource == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing resource")
}
resourceCloner, ok := resource.(Cloneable)
if !ok {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "resource is not Cloneable")
}
metadata, err := r.stdMetadata(ctx, resource)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error getting metadata"))
}
metadata["op-type"] = []string{oplog.OpType_OP_TYPE_UPDATE.String()}
dbOpts := []db.Option{
db.WithVersion(&version),
}
opts := getOpts(opt...)
if opts.withSkipVetForWrite {
dbOpts = append(dbOpts, db.WithSkipVetForWrite(true))
}
var scope *Scope
switch t := resource.(type) {
case *Scope:
scope = t
default:
scope, err = resource.GetScope(ctx, r.reader)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get scope"))
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
dbOpts = append(dbOpts, db.WithOplog(oplogWrapper, metadata))
var rowsUpdated int
var returnedResource any
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
returnedResource = resourceCloner.Clone()
rowsUpdated, err = w.Update(
ctx,
returnedResource,
fieldMaskPaths,
setToNullPaths,
dbOpts...,
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsUpdated > 1 {
// return err, which will result in a rollback of the update
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
}
return nil
},
)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return returnedResource.(Resource), rowsUpdated, nil
}
// delete will delete an iam resource in the db repository with an oplog entry
func (r *Repository) delete(ctx context.Context, resource Resource, _ ...Option) (int, error) {
const op = "iam.(Repository).delete"
if resource == nil {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing resource")
}
resourceCloner, ok := resource.(Cloneable)
if !ok {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "resource is not Cloneable")
}
metadata, err := r.stdMetadata(ctx, resource)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error getting metadata"))
}
metadata["op-type"] = []string{oplog.OpType_OP_TYPE_DELETE.String()}
scope, err := resource.GetScope(ctx, r.reader)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get scope"))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var rowsDeleted int
var deleteResource any
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
deleteResource = resourceCloner.Clone()
rowsDeleted, err = w.Delete(
ctx,
deleteResource,
db.WithOplog(oplogWrapper, metadata),
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsDeleted > 1 {
// return err, which will result in a rollback of the delete
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return rowsDeleted, nil
}
func (r *Repository) stdMetadata(ctx context.Context, resource Resource) (oplog.Metadata, error) {
const op = "iam.(Repository).stdMetadata"
if s, ok := resource.(*Scope); ok {
newScope := AllocScope()
newScope.PublicId = s.PublicId
newScope.Type = s.Type
if newScope.Type == "" {
if err := r.reader.LookupByPublicId(ctx, &newScope); err != nil {
return nil, errors.Wrap(ctx, ErrMetadataScopeNotFound, op)
}
}
switch newScope.Type {
case scope.Global.String(), scope.Org.String():
return oplog.Metadata{
"resource-public-id": []string{resource.GetPublicId()},
"scope-id": []string{newScope.PublicId},
"scope-type": []string{newScope.Type},
"resource-type": []string{resource.GetResourceType().String()},
}, nil
case scope.Project.String():
return oplog.Metadata{
"resource-public-id": []string{resource.GetPublicId()},
"scope-id": []string{newScope.ParentId},
"scope-type": []string{newScope.Type},
"resource-type": []string{resource.GetResourceType().String()},
}, nil
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("not a supported scope for metadata: %s", s.Type))
}
}
scope, err := resource.GetScope(ctx, r.reader)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get scope"))
}
if scope == nil {
return nil, errors.E(ctx, errors.WithOp(op), errors.WithMsg("nil scope"))
}
return oplog.Metadata{
"resource-public-id": []string{resource.GetPublicId()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-type": []string{resource.GetResourceType().String()},
}, nil
}
func contains(ss []string, t string) bool {
for _, s := range ss {
if strings.EqualFold(s, t) {
return true
}
}
return false
}