mirror of https://github.com/hashicorp/boundary
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 c0a318c583)
pull/5128/head
parent
4d8a7dd39a
commit
b04de5ac4b
@ -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)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in new issue