Skip to content

Commit

Permalink
Add implicit-scopes resource type to cache (#5053)
Browse files Browse the repository at this point in the history
This allows getting a list of scope IDs known by the cache via cached
targets and sessions. It is not refreshed from the controller. The name
contains `implicit` in case we ever want e.g. `scopes`.
  • Loading branch information
jefferai committed Sep 11, 2024
1 parent 72a4d09 commit fe5ed5b
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 10 deletions.
40 changes: 37 additions & 3 deletions internal/clientcache/cmd/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
cachecmd "github.com/hashicorp/boundary/internal/clientcache/cmd/cache"
Expand All @@ -34,6 +35,7 @@ var (
"resolvable-aliases",
"targets",
"sessions",
"implicit-scopes",
}

errCacheNotRunning = stderrors.New("The cache process is not running.")
Expand Down Expand Up @@ -181,19 +183,24 @@ func (c *SearchCommand) Run(args []string) int {
return base.CommandCliError
}
default:
if result.Incomplete {
c.UI.Warn("The maximum result set size was reached and the search results are incomplete. Please narrow your search or adjust the -max-result-set-size parameter.")
}
switch {
case len(result.ResolvableAliases) > 0:
c.UI.Output(printAliasListTable(result.ResolvableAliases))
case len(result.Targets) > 0:
c.UI.Output(printTargetListTable(result.Targets))
case len(result.Sessions) > 0:
c.UI.Output(printSessionListTable(result.Sessions))
case len(result.ImplicitScopes) > 0:
c.UI.Output(printImplicitScopesListTable(result.ImplicitScopes))
default:
c.UI.Output("No items found")
}

// Put this at the end or people may not see it as they may not scroll
// all the way up.
if result.Incomplete {
c.UI.Warn("The maximum result set size was reached and the search results are incomplete. Please narrow your search or adjust the -max-result-set-size parameter.")
}
}
return base.CommandSuccess
}
Expand Down Expand Up @@ -449,6 +456,33 @@ func printSessionListTable(items []*sessions.Session) string {
return base.WrapForHelpText(output)
}

func printImplicitScopesListTable(items []*scopes.Scope) string {
if len(items) == 0 {
return "No implicit scopes found"
}
var output []string
output = []string{
"",
"Scope information:",
}
for i, item := range items {
if i > 0 {
output = append(output, "")
}
if item.Id != "" {
output = append(output,
fmt.Sprintf(" ID: %s", item.Id),
)
} else {
output = append(output,
fmt.Sprintf(" ID: %s", "(not available)"),
)
}
}

return base.WrapForHelpText(output)
}

type filterBy struct {
flagFilter string
flagQuery string
Expand Down
114 changes: 114 additions & 0 deletions internal/clientcache/internal/cache/repository_implicit_scopes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package cache

import (
"context"
"fmt"
"slices"

"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/internal/errors"
)

func (r *Repository) ListImplicitScopes(ctx context.Context, authTokenId string, opt ...Option) (*SearchResult, error) {
const op = "cache.(Repository).ListImplicitScopes"
switch {
case authTokenId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
}
ret, err := r.searchImplicitScopes(ctx, "true", nil, append(opt, withAuthTokenId(authTokenId))...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return ret, nil
}

// QueryImplicitScopes is not supported currently so we return an error message
func (r *Repository) QueryImplicitScopes(ctx context.Context, authTokenId, query string, opt ...Option) (*SearchResult, error) {
const op = "cache.(Repository).QueryImplicitScopes"

// Internal is used as we have checks at the handler level to ensure this
// can't be used so it's an internal error if we actually call this
// function.
return nil, errors.New(ctx, errors.Internal, op, "querying implicit scopes is not supported")
}

func (r *Repository) searchImplicitScopes(ctx context.Context, condition string, searchArgs []any, opt ...Option) (*SearchResult, error) {
const op = "cache.(Repository).searchImplicitScopes"
switch {
case condition == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "condition is missing")
}

opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
switch {
case opts.withAuthTokenId != "" && opts.withUserId != "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "both user id and auth token id were provided")
case opts.withAuthTokenId == "" && opts.withUserId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "neither user id nor auth token id were provided")

// In these cases we append twice because we're doing a union of two tables
case opts.withAuthTokenId != "":
condition = "where fk_user_id in (select user_id from auth_token where id = ?)"
searchArgs = append(searchArgs, opts.withAuthTokenId, opts.withAuthTokenId)
case opts.withUserId != "":
condition = "where fk_user_id = ?"
searchArgs = append(searchArgs, opts.withUserId, opts.withUserId)
}

const unionQueryBase = `
select distinct fk_user_id, scope_id from session
%s
union
select distinct fk_user_id, scope_id from target
%s
`
unionQuery := fmt.Sprintf(unionQueryBase, condition, condition)

rows, err := r.rw.Query(ctx, unionQuery, searchArgs)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer rows.Close()

type ScopeIdsResult struct {
FkUserId string `gorm:"primaryKey"`
ScopeId string `gorm:"default:null"`
}

var scopeIdsResults []ScopeIdsResult
for rows.Next() {
var res ScopeIdsResult
if err := r.rw.ScanRows(ctx, rows, &res); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
scopeIdsResults = append(scopeIdsResults, res)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op)
}

dedupMap := make(map[string]struct{}, len(scopeIdsResults))
for _, res := range scopeIdsResults {
dedupMap[res.ScopeId] = struct{}{}
}
scopeIds := make([]string, 0, len(dedupMap))
for k := range dedupMap {
scopeIds = append(scopeIds, k)
}
slices.Sort(scopeIds)

sr := &SearchResult{
ImplicitScopes: make([]*scopes.Scope, 0, len(dedupMap)),
}
for _, scopeId := range scopeIds {
sr.ImplicitScopes = append(sr.ImplicitScopes, &scopes.Scope{Id: scopeId})
}

return sr, nil
}
124 changes: 124 additions & 0 deletions internal/clientcache/internal/cache/repository_implicit_scopes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package cache

import (
"context"
"sync"
"testing"

"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)

func TestRepository_ImplicitScopes(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)

addr := "address"
u1 := &user{
Id: "u1",
Address: addr,
}
at1 := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u1.Id,
}
kt1 := KeyringToken{KeyringType: "k1", TokenName: "t1", AuthTokenId: at1.Id}

u2 := &user{
Id: "u2",
Address: addr,
}
at2 := &authtokens.AuthToken{
Id: "at_2",
Token: "at_2_token",
UserId: u2.Id,
}
kt2 := KeyringToken{KeyringType: "k2", TokenName: "t2", AuthTokenId: at2.Id}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at1,
{"k2", "t2"}: at2,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
require.NoError(t, r.AddKeyringToken(ctx, addr, kt2))

var expectedScopes []*scopes.Scope

ts := []*targets.Target{
target("1"),
target("2"),
target("3"),
}
require.NoError(t, r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{ts}, [][]string{nil}))))

for _, t := range ts {
expectedScopes = append(expectedScopes, &scopes.Scope{
Id: t.ScopeId,
})
}

ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "status1",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
},
{
Id: "ttcp_2",
Status: "status2",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
},
{
Id: "ttcp_3",
Status: "status3",
Endpoint: "address3",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
},
}
require.NoError(t, r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil}))))

expectedScopes = append(expectedScopes, &scopes.Scope{
Id: ss[0].ScopeId,
})

t.Run("wrong user gets no implicit scopes", func(t *testing.T) {
l, err := r.ListImplicitScopes(ctx, kt2.AuthTokenId)
require.NoError(t, err)
assert.Empty(t, l.ImplicitScopes)
})
t.Run("correct token gets implicit scopes from listing", func(t *testing.T) {
l, err := r.ListImplicitScopes(ctx, kt1.AuthTokenId)
require.NoError(t, err)
assert.Len(t, l.ImplicitScopes, len(expectedScopes))
assert.ElementsMatch(t, l.ImplicitScopes, expectedScopes)
})
t.Run("querying returns error", func(t *testing.T) {
_, err := r.QueryImplicitScopes(ctx, kt1.AuthTokenId, "anything")
require.Error(t, err)
})
}
20 changes: 19 additions & 1 deletion internal/clientcache/internal/cache/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/errors"
Expand All @@ -23,11 +24,12 @@ const (
ResolvableAliases SearchableResource = "resolvable-aliases"
Targets SearchableResource = "targets"
Sessions SearchableResource = "sessions"
ImplicitScopes SearchableResource = "implicit-scopes"
)

func (r SearchableResource) Valid() bool {
switch r {
case ResolvableAliases, Targets, Sessions:
case ResolvableAliases, Targets, Sessions, ImplicitScopes:
return true
}
return false
Expand All @@ -41,6 +43,8 @@ func ToSearchableResource(s string) SearchableResource {
return Targets
case strings.EqualFold(s, string(Sessions)):
return Sessions
case strings.EqualFold(s, string(ImplicitScopes)):
return ImplicitScopes
}
return Unknown
}
Expand All @@ -64,6 +68,7 @@ type SearchResult struct {
ResolvableAliases []*aliases.Alias `json:"resolvable_aliases,omitempty"`
Targets []*targets.Target `json:"targets,omitempty"`
Sessions []*sessions.Session `json:"sessions,omitempty"`
ImplicitScopes []*scopes.Scope `json:"implicit_scopes,omitempty"`

// Incomplete is true if the search results are incomplete, that is, we are
// returning only a subset based on the max result set size
Expand Down Expand Up @@ -125,6 +130,19 @@ func NewSearchService(ctx context.Context, repo *Repository) (*SearchService, er
in.Sessions = finalResults
},
},
ImplicitScopes: &resourceSearchFns[*scopes.Scope]{
list: repo.ListImplicitScopes,
query: repo.QueryImplicitScopes,
filter: func(in *SearchResult, e *bexpr.Evaluator) {
finalResults := make([]*scopes.Scope, 0, len(in.ImplicitScopes))
for _, item := range in.ImplicitScopes {
if m, err := e.Evaluate(filterItem{item}); err == nil && m {
finalResults = append(finalResults, item)
}
}
in.ImplicitScopes = finalResults
},
},
},
}, nil
}
Expand Down
Loading

0 comments on commit fe5ed5b

Please sign in to comment.