Skip to content

Commit

Permalink
Merge pull request #4662 from hashicorp/backport/alanknight_alias_lis…
Browse files Browse the repository at this point in the history
…tresolvable/willingly-flowing-gator

This pull request was automerged via backport-assistant
  • Loading branch information
hc-github-team-secure-boundary authored Apr 18, 2024
2 parents 7a07950 + 9cd467e commit 3b311b8
Show file tree
Hide file tree
Showing 26 changed files with 3,082 additions and 436 deletions.
220 changes: 220 additions & 0 deletions internal/alias/target/repository_alias_list_resolvable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package target

import (
"context"
"database/sql"
"fmt"
"strings"
"time"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/perms"
)

// targetAndScopeIdsForDestinations returns the target ids for which there is
// at least one permission. If all targets in a specific scope are granted
// permission for an action, then the scope id is in the returned scope id slice.
func targetAndScopeIdsForDestinations(perms []perms.Permission) ([]string, []string) {
var targetIds, scopeIds []string
for _, perm := range perms {
switch {
case perm.All:
scopeIds = append(scopeIds, perm.ScopeId)
case len(perm.ResourceIds) > 0:
targetIds = append(targetIds, perm.ResourceIds...)
}
}
return targetIds, scopeIds
}

// listResolvableAliases lists aliases which have a destination id set to that
// of a target for which there is permission in the provided slice of permissions.
// Only WithLimit and WithStartPageAfterItem options are supported.
func (r *Repository) listResolvableAliases(ctx context.Context, permissions []perms.Permission, opt ...Option) ([]*Alias, time.Time, error) {
const op = "target.(Repository).listResolvableAliases"
switch {
case len(permissions) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions")
}
toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions)

opts, err := getOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}

limit := r.defaultLimit
switch {
case opts.withLimit > 0:
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
case opts.withLimit < 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative")
}

var args []any
var destinationIdClauses []string
if len(toTargetIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids")
args = append(args, sql.Named("target_ids", toTargetIds))
}
if len(toTargetsInScopeIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)")
args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds))
}
if len(destinationIdClauses) == 0 {
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided")
}

whereClause := fmt.Sprintf("destination_id is not null and (%s)", strings.Join(destinationIdClauses, " or "))

if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_create_time", opts.withStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc"), db.WithDebug(true)}
return r.queryAliases(ctx, whereClause, args, dbOpts...)
}

// listResolvableAliasesRefresh lists aliases limited by the list
// permissions of the repository.
// Supported options:
// - withLimit
// - withStartPageAfterItem
func (r *Repository) listResolvableAliasesRefresh(ctx context.Context, updatedAfter time.Time, permissions []perms.Permission, opt ...Option) ([]*Alias, time.Time, error) {
const op = "target.(Repository).listResolvableAliasesRefresh"

switch {
case updatedAfter.IsZero():
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
case len(permissions) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions")
}
toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions)

opts, err := getOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}

limit := r.defaultLimit
switch {
case opts.withLimit > 0:
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
case opts.withLimit < 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative")
}

var args []any
var destinationIdClauses []string
if len(toTargetIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids")
args = append(args, sql.Named("target_ids", toTargetIds))
}
if len(toTargetsInScopeIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)")
args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds))
}
if len(destinationIdClauses) == 0 {
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided")
}

whereClause := fmt.Sprintf("update_time > @updated_after_time and destination_id is not null and (%s)",
strings.Join(destinationIdClauses, " or "))
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
)
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_update_time", opts.withStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}

dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")}
return r.queryAliases(ctx, whereClause, args, dbOpts...)
}

// listRemovedResolvableIds lists the public IDs of any aliases deleted since
// the timestamp provided or which have been updated since the timestamp provided
// and do not have a destination id set to the id of a target for which there
// are permissions in the provided slice of permissions.
func (r *Repository) listRemovedResolvableAliasIds(ctx context.Context, since time.Time, permissions []perms.Permission) ([]string, time.Time, error) {
const op = "target.(Repository).listRemovedResolvableIds"
switch {
case len(permissions) == 0:
// while a lack of permissions is one way for targets to not be included
// in the list of resolvable aliases, if permissions were always empty
// then no aliases would have been returned in the first place and so
// no ids would need to be removed. If permissions were changed to
// become empty, then the list token would be invalidated and we shouldnt
// have made it here, so it is an error for an empty slice of permissions
// to be provided.
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions")
}
toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions)

var args []any
var destinationIdClauses []string
if len(toTargetIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id not in @target_ids")
args = append(args, sql.Named("target_ids", toTargetIds))
}
if len(toTargetsInScopeIds) > 0 {
destinationIdClauses = append(destinationIdClauses, "destination_id not in (select public_id from target where project_id in @target_scope_ids)")
args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds))
}
if len(destinationIdClauses) == 0 {
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided")
}
whereClause := fmt.Sprintf("update_time > @updated_after_time and (destination_id is null or (%s))",
strings.Join(destinationIdClauses, " and "))
args = append(args,
sql.Named("updated_after_time", timestamp.New(since)),
)

// The calculating of the deleted aliases and the non matching alises
// must happen in the same transaction to ensure consistency.
var notMatchingAliases []*Alias
var deletedAliases []*deletedAlias
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error {
if err := r.SearchWhere(ctx, &deletedAliases, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted aliases"))
}

var inRet []*Alias
if err := r.SearchWhere(ctx, &inRet, whereClause, args); err != nil {
return errors.Wrap(ctx, err, op)
}
notMatchingAliases = inRet

var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp"))
}

return nil
}); err != nil {
return nil, time.Time{}, err
}
var aliasIds []string
for _, da := range deletedAliases {
aliasIds = append(aliasIds, da.PublicId)
}
for _, na := range notMatchingAliases {
aliasIds = append(aliasIds, na.PublicId)
}
return aliasIds, transactionTimestamp, nil
}
45 changes: 45 additions & 0 deletions internal/alias/target/service_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
)

// ListAliases lists up to page size aliases, filtering out entries that
Expand Down Expand Up @@ -51,3 +52,47 @@ func ListAliases(

return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount)
}

// ListResolvableAliases lists up to page size aliases, filtering out entries that
// do not pass the filter item function. It will automatically request
// more aliases from the database, at page size chunks, to fill the page. Only
// aliases which have the destination id set to a target for which there are
// permissions in the provided slice will be returned.
// It returns a new list token used to continue pagination or refresh items.
// Aliases are ordered by create time descending (most recently created first).
func ListResolvableAliases(
ctx context.Context,
grantsHash []byte,
pageSize int,
repo *Repository,
permissions []perms.Permission,
) (*pagination.ListResponse[*Alias], error) {
const op = "target.ListResolvableAliases"

switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case len(permissions) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target permissions")
}

listItemsFn := func(ctx context.Context, lastPageItem *Alias, limit int) ([]*Alias, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
}
return repo.listResolvableAliases(ctx, permissions, opts...)
}

return pagination.List(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount)
}

func alwaysTrueFilterFn(context.Context, *Alias) (bool, error) {
return true, nil
}
56 changes: 56 additions & 0 deletions internal/alias/target/service_list_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/resource"
)

Expand Down Expand Up @@ -68,3 +69,58 @@ func ListAliasesPage(

return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, tok)
}

// ListResolvableAliasesPage lists up to page size aliases, filtering out entries that
// do not pass the filter item function. It will automatically request
// more aliases from the database, at page size chunks, to fill the page.
// Only aliases which resolve to a target for which there are permissions in the
// included slice of permissions are returned.
// It will start its paging based on the information in the token.
// It returns a new list token used to continue pagination or refresh items.
// Aliases are ordered by create time descending (most recently created first).
func ListResolvableAliasesPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
tok *listtoken.Token,
repo *Repository,
perms []perms.Permission,
) (*pagination.ListResponse[*Alias], error) {
const op = "target.ListResolvableAliasesPage"

switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case tok.ResourceType != resource.Alias:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a alias resource type")
case len(perms) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing permissions")
}
if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component")
}

listItemsFn := func(ctx context.Context, lastPageItem *Alias, limit int) ([]*Alias, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
} else {
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
opts = append(opts, WithStartPageAfterItem(lastItem))
}
return repo.listResolvableAliases(ctx, perms, opts...)
}

return pagination.ListPage(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount, tok)
}
Loading

0 comments on commit 3b311b8

Please sign in to comment.