Add implicit-scopes resource type to cache (#5053)
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`. (cherry picked from commit c0a318c58367d8ec55365447f41cf007eab64ce3)
This commit is contained in:
1 parent
4d8a7dd39a
commit
b04de5ac4b
5 files changed
+318
-10
No files matched your search
@@ -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"
|
||||
@@ -34,6 +35,7 @@ var (
|
||||
"resolvable-aliases",
|
||||
"targets",
|
||||
"sessions",
|
||||
"implicit-scopes",
|
||||
}
|
||||
|
||||
errCacheNotRunning = stderrors.New("The cache process is not running.")
|
||||
@@ -181,9 +183,6 @@ 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))
|
||||
@@ -191,9 +190,17 @@ func (c *SearchCommand) Run(args []string) int {
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
+19
-1
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -12,6 +12,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"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
@@ -26,6 +27,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 bool `json:"incomplete,omitempty"`
|
||||
}
|
||||
|
||||
@@ -61,6 +63,8 @@ func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository, refreshSe
|
||||
authTokenId := q.Get(authTokenIdKey)
|
||||
maxResultSetSizeStr := q.Get(maxResultSetSizeKey)
|
||||
maxResultSetSizeInt, maxResultSetSizeIntErr := strconv.Atoi(maxResultSetSizeStr)
|
||||
query := q.Get(queryKey)
|
||||
filter := q.Get(filterKey)
|
||||
|
||||
searchableResource := cache.ToSearchableResource(resource)
|
||||
switch {
|
||||
@@ -84,6 +88,18 @@ func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository, refreshSe
|
||||
event.WriteError(ctx, op, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s must be greater than or equal to -1", maxResultSetSizeStr)))
|
||||
writeError(w, fmt.Sprintf("%s must be greater than or equal to -1", maxResultSetSizeStr), http.StatusBadRequest)
|
||||
return
|
||||
case searchableResource == cache.ImplicitScopes && maxResultSetSizeStr != "":
|
||||
event.WriteError(ctx, op, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("max result set size is not supported for resource %q", resource)))
|
||||
writeError(w, fmt.Sprintf("max result set size is not supported for resource %q", resource), http.StatusBadRequest)
|
||||
return
|
||||
case searchableResource == cache.ImplicitScopes && query != "":
|
||||
event.WriteError(ctx, op, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("query is not supported for resource %q", resource)))
|
||||
writeError(w, fmt.Sprintf("query is not supported for resource %q", resource), http.StatusBadRequest)
|
||||
return
|
||||
case searchableResource == cache.ImplicitScopes && filter != "":
|
||||
event.WriteError(ctx, op, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("filter is not supported for resource %q", resource)))
|
||||
writeError(w, fmt.Sprintf("filter is not supported for resource %q", resource), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := repo.LookupToken(reqCtx, authTokenId, cache.WithUpdateLastAccessedTime(true))
|
||||
@@ -118,14 +134,15 @@ func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository, refreshSe
|
||||
// Refresh the resources for the provided user, if possible. This is best
|
||||
// effort, so if there is any problem refreshing, we just log the error
|
||||
// and move on to handling the search request.
|
||||
if err := refreshService.RefreshForSearch(reqCtx, authTokenId, searchableResource, opts...); err != nil {
|
||||
// we don't stop the search, we just log that the inline refresh failed
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("when refreshing the resources inline for search", "auth_token_id", authTokenId, "resource", searchableResource))
|
||||
switch searchableResource {
|
||||
case cache.ImplicitScopes:
|
||||
default:
|
||||
if err := refreshService.RefreshForSearch(reqCtx, authTokenId, searchableResource, opts...); err != nil {
|
||||
// we don't stop the search, we just log that the inline refresh failed
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("when refreshing the resources inline for search", "auth_token_id", authTokenId, "resource", searchableResource))
|
||||
}
|
||||
}
|
||||
|
||||
query := r.URL.Query().Get(queryKey)
|
||||
filter := r.URL.Query().Get(filterKey)
|
||||
|
||||
res, err := s.Search(reqCtx, cache.SearchParams{
|
||||
AuthTokenId: authTokenId,
|
||||
Resource: searchableResource,
|
||||
@@ -166,6 +183,7 @@ func toApiResult(sr *cache.SearchResult) *SearchResult {
|
||||
ResolvableAliases: sr.ResolvableAliases,
|
||||
Targets: sr.Targets,
|
||||
Sessions: sr.Sessions,
|
||||
ImplicitScopes: sr.ImplicitScopes,
|
||||
Incomplete: sr.Incomplete,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user