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.
299 lines
9.4 KiB
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
|
|
}
|