Store and use refresh tokens in the client cache (#3857)
* Refresh resources using refresh tokens
This commit is contained in:
1 parent
5daef58f54
commit
b2fec2e4f0
22 files changed
+1388
-105
No files matched your search
+5
-3
@@ -33,7 +33,9 @@ func Test_GetOpts(t *testing.T) {
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithTargetRetrievalFunc", func(t *testing.T) {
|
||||
var f TargetRetrievalFunc = func(ctx context.Context, keyringstring, tokenName string) ([]*targets.Target, error) { return nil, nil }
|
||||
var f TargetRetrievalFunc = func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*targets.Target, []string, RefreshTokenValue, error) {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
opts, err := getOpts(WithTargetRetrievalFunc(f))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -44,8 +46,8 @@ func Test_GetOpts(t *testing.T) {
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithSessionRetrievalFunc", func(t *testing.T) {
|
||||
var f SessionRetrievalFunc = func(ctx context.Context, keyringstring, tokenName string) ([]*sessions.Session, error) {
|
||||
return nil, nil
|
||||
var f SessionRetrievalFunc = func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*sessions.Session, []string, RefreshTokenValue, error) {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
opts, err := getOpts(WithSessionRetrievalFunc(f))
|
||||
require.NoError(t, err)
|
||||
|
||||
+87
-47
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,10 +25,42 @@ import (
|
||||
// testStaticResourceRetrievalFunc returns a function that always returns the
|
||||
// provided slice and a nil error. The returned function can be passed into the
|
||||
// options that provide a resource retrieval func such as
|
||||
// WithTargetRetrievalFunc and WithSessionRetrievalFunc.
|
||||
func testStaticResourceRetrievalFunc[T any](ret []T) func(context.Context, string, string) ([]T, error) {
|
||||
return func(ctx context.Context, s1, s2 string) ([]T, error) {
|
||||
return ret, nil
|
||||
// WithTargetRetrievalFunc and WithSessionRetrievalFunc. The provided refresh
|
||||
// token determines the returned value and is a string representation of an
|
||||
// incrementing integer. This integer is the index into the provided return
|
||||
// values and once it reaches the length of the provided slice it returns an
|
||||
// empty slice and the same refresh token repeatedly.
|
||||
func testStaticResourceRetrievalFunc[T any](t *testing.T, ret [][]T, removed [][]string) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
||||
t.Helper()
|
||||
require.Equal(t, len(ret), len(removed), "returned slice and removed slice must be the same length")
|
||||
return func(ctx context.Context, s1, s2 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
||||
index := 0
|
||||
if refToken != "" {
|
||||
var err error
|
||||
index, err = strconv.Atoi(string(refToken))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(ret) == 0:
|
||||
return nil, nil, "", nil
|
||||
case index > 0 && index >= len(ret):
|
||||
return []T{}, []string{}, RefreshTokenValue(fmt.Sprintf("%d", index)), nil
|
||||
default:
|
||||
return ret[index], removed[index], RefreshTokenValue(fmt.Sprintf("%d", index+1)), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testErroringForRefreshTokenRetrievalFunc returns a refresh token error when
|
||||
// the refresh token is not empty. This is useful for testing behavior when
|
||||
// the refresh token has expired or is otherwise invalid.
|
||||
func testErroringForRefreshTokenRetrievalFunc[T any](t *testing.T, ret []T) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
||||
return func(ctx context.Context, s1, s2 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
||||
if refToken != "" {
|
||||
return nil, nil, "", api.ErrInvalidRefreshToken
|
||||
}
|
||||
return ret, nil, "1", nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,32 +330,32 @@ func TestRefresh(t *testing.T) {
|
||||
target("1"),
|
||||
target("2"),
|
||||
target("3"),
|
||||
target("4"),
|
||||
}
|
||||
assert.NoError(t, rs.Refresh(ctx,
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
||||
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
||||
require.Equal(t, boundaryAddr, addr)
|
||||
require.Equal(t, at.Token, token)
|
||||
return retTargets, nil
|
||||
})))
|
||||
opts := []Option{
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
|
||||
[][]*targets.Target{
|
||||
retTargets[:3],
|
||||
retTargets[3:],
|
||||
},
|
||||
[][]string{
|
||||
nil,
|
||||
{retTargets[0].Id, retTargets[1].Id},
|
||||
},
|
||||
)),
|
||||
}
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
|
||||
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retTargets, cachedTargets)
|
||||
assert.ElementsMatch(t, retTargets[:3], cachedTargets)
|
||||
|
||||
t.Run("empty response clears it out", func(t *testing.T) {
|
||||
assert.NoError(t, rs.Refresh(ctx,
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
||||
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
||||
require.Equal(t, boundaryAddr, addr)
|
||||
require.Equal(t, at.Token, token)
|
||||
return nil, nil
|
||||
})))
|
||||
|
||||
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, cachedTargets)
|
||||
})
|
||||
// Second call removes the first 2 resources from the cache and adds the last
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
cachedTargets, err = r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retTargets[2:], cachedTargets)
|
||||
})
|
||||
|
||||
t.Run("set sessions", func(t *testing.T) {
|
||||
@@ -330,42 +363,49 @@ func TestRefresh(t *testing.T) {
|
||||
session("1"),
|
||||
session("2"),
|
||||
session("3"),
|
||||
session("4"),
|
||||
}
|
||||
assert.NoError(t, rs.Refresh(ctx,
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(retSess))))
|
||||
|
||||
opts := []Option{
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
|
||||
[][]*sessions.Session{
|
||||
retSess[:3],
|
||||
retSess[3:],
|
||||
},
|
||||
[][]string{
|
||||
nil,
|
||||
{retSess[0].Id, retSess[1].Id},
|
||||
},
|
||||
)),
|
||||
}
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
cachedSessions, err := r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retSess, cachedSessions)
|
||||
assert.ElementsMatch(t, retSess[:3], cachedSessions)
|
||||
|
||||
t.Run("empty response clears it out", func(t *testing.T) {
|
||||
assert.NoError(t, rs.Refresh(ctx,
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil))))
|
||||
|
||||
cachedTargets, err := r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, cachedTargets)
|
||||
})
|
||||
// Second call removes the first 2 resources from the cache and adds the last
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retSess[2:], cachedSessions)
|
||||
})
|
||||
|
||||
t.Run("error propogates up", func(t *testing.T) {
|
||||
innerErr := errors.New("test error")
|
||||
err := rs.Refresh(ctx,
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
||||
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
||||
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*targets.Target, []string, RefreshTokenValue, error) {
|
||||
require.Equal(t, boundaryAddr, addr)
|
||||
require.Equal(t, at.Token, token)
|
||||
return nil, innerErr
|
||||
return nil, nil, "", innerErr
|
||||
}))
|
||||
assert.ErrorContains(t, err, innerErr.Error())
|
||||
err = rs.Refresh(ctx,
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
||||
WithSessionRetrievalFunc(func(ctx context.Context, addr, token string) ([]*sessions.Session, error) {
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
||||
WithSessionRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*sessions.Session, []string, RefreshTokenValue, error) {
|
||||
require.Equal(t, boundaryAddr, addr)
|
||||
require.Equal(t, at.Token, token)
|
||||
return nil, innerErr
|
||||
return nil, nil, "", innerErr
|
||||
}))
|
||||
assert.ErrorContains(t, err, innerErr.Error())
|
||||
})
|
||||
@@ -385,8 +425,8 @@ func TestRefresh(t *testing.T) {
|
||||
assert.Len(t, us, 1)
|
||||
|
||||
rs.Refresh(ctx,
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)))
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)))
|
||||
|
||||
ps, err = r.listTokens(ctx, u)
|
||||
require.NoError(t, err)
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ func NewRepository(ctx context.Context, conn *db.DB, idToAuthToken *sync.Map, ke
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SaveError(ctx context.Context, u *user, resourceType string, err error) error {
|
||||
func (r *Repository) SaveError(ctx context.Context, u *user, resourceType resourceType, err error) error {
|
||||
const op = "cache.(Repository).StoreError"
|
||||
switch {
|
||||
case resourceType == "":
|
||||
@@ -70,7 +70,7 @@ func (r *Repository) SaveError(ctx context.Context, u *user, resourceType string
|
||||
}
|
||||
apiErr := &ApiError{
|
||||
UserId: u.Id,
|
||||
ResourceType: resourceType,
|
||||
ResourceType: string(resourceType),
|
||||
Error: err.Error(),
|
||||
}
|
||||
onConflict := db.OnConflict{
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
// RefreshTokenValue is the the type for the actual refresh token value handled
|
||||
// by the client cache.
|
||||
type RefreshTokenValue string
|
||||
|
||||
// lookupRefreshToken returns the last known valid refresh token or an empty
|
||||
// string if one is unkonwn. No error is returned if no valid refresh token is
|
||||
// found.
|
||||
func (r *Repository) lookupRefreshToken(ctx context.Context, u *user, resourceType resourceType) (RefreshTokenValue, error) {
|
||||
const op = "cache.(Repsoitory).lookupRefreshToken"
|
||||
switch {
|
||||
case util.IsNil(u):
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !resourceType.valid():
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
|
||||
rt := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: resourceType,
|
||||
}
|
||||
if err := r.rw.LookupById(ctx, rt); err != nil {
|
||||
if errors.Is(err, dbw.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return rt.RefreshToken, nil
|
||||
}
|
||||
|
||||
// deleteRefreshToken deletes the refresh token for the provided user and resource type
|
||||
func (r *Repository) deleteRefreshToken(ctx context.Context, u *user, rType resourceType) error {
|
||||
const op = "cache.(Repository).deleteRefreshToken"
|
||||
switch {
|
||||
case util.IsNil(u):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !rType.valid():
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
|
||||
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
rt := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: rType,
|
||||
}
|
||||
n, err := w.Delete(ctx, rt)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if n > 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "attempted to delete a single resource but multiple were resource deletions were attempted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertRefreshToken(ctx context.Context, writer db.Writer, u *user, rt resourceType, tok RefreshTokenValue) error {
|
||||
const op = "cache.upsertRefreshToken"
|
||||
switch {
|
||||
case util.IsNil(writer):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
||||
case !writer.IsTx(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't in a transaction")
|
||||
case util.IsNil(u):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !rt.valid():
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
|
||||
refTok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: rt,
|
||||
RefreshToken: tok,
|
||||
}
|
||||
|
||||
switch tok {
|
||||
case "":
|
||||
writer.Delete(ctx, refTok)
|
||||
default:
|
||||
onConflict := &db.OnConflict{
|
||||
Target: db.Columns{"user_id", "resource_type"},
|
||||
Action: db.SetColumns([]string{"refresh_token"}),
|
||||
}
|
||||
if err := writer.Create(ctx, refTok, db.WithOnConflict(onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type resourceType string
|
||||
|
||||
const (
|
||||
unknownResourceType resourceType = "unknown"
|
||||
targetResourceType resourceType = "target"
|
||||
sessionResourceType resourceType = "session"
|
||||
)
|
||||
|
||||
func (r resourceType) valid() bool {
|
||||
switch r {
|
||||
case targetResourceType, sessionResourceType:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type refreshToken struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType resourceType `gorm:"primaryKey"`
|
||||
RefreshToken RefreshTokenValue
|
||||
}
|
||||
|
||||
func (*refreshToken) TableName() string {
|
||||
return "refresh_token"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLookupRefreshToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("nil user", func(t *testing.T) {
|
||||
_, err := r.lookupRefreshToken(ctx, nil, targetResourceType)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "user is nil")
|
||||
})
|
||||
|
||||
t.Run("user id is empty", func(t *testing.T) {
|
||||
_, err := r.lookupRefreshToken(ctx, &user{Address: "addr"}, targetResourceType)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "user id is empty")
|
||||
})
|
||||
|
||||
t.Run("resource type is invalid", func(t *testing.T) {
|
||||
_, err := r.lookupRefreshToken(ctx, &user{Id: "something", Address: "addr"}, resourceType("invalid"))
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "resource type is invalid")
|
||||
})
|
||||
|
||||
t.Run("unknown user", func(t *testing.T) {
|
||||
got, err := r.lookupRefreshToken(ctx, &user{Id: "unkonwnUser", Address: "addr"}, targetResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
})
|
||||
|
||||
t.Run("no refresh token", func(t *testing.T) {
|
||||
known := &user{Id: "known", Address: "addr"}
|
||||
require.NoError(t, r.rw.Create(ctx, known))
|
||||
|
||||
got, err := r.lookupRefreshToken(ctx, known, targetResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
})
|
||||
|
||||
t.Run("got refresh token", func(t *testing.T) {
|
||||
token := RefreshTokenValue("something")
|
||||
known := &user{Id: "withrefreshtoken", Address: "addr"}
|
||||
require.NoError(t, r.rw.Create(ctx, known))
|
||||
|
||||
r.rw.DoTx(ctx, 1, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
require.NoError(t, upsertRefreshToken(ctx, w, known, targetResourceType, token))
|
||||
return nil
|
||||
})
|
||||
|
||||
got, err := r.lookupRefreshToken(ctx, known, targetResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteRefreshTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("nil user", func(t *testing.T) {
|
||||
err := r.deleteRefreshToken(ctx, nil, targetResourceType)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "user is nil")
|
||||
})
|
||||
|
||||
t.Run("no user id", func(t *testing.T) {
|
||||
err := r.deleteRefreshToken(ctx, &user{Address: "addr"}, targetResourceType)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "user id is empty")
|
||||
})
|
||||
|
||||
t.Run("invalid resource type", func(t *testing.T) {
|
||||
err := r.deleteRefreshToken(ctx, &user{Id: "id", Address: "addr"}, "this is invalid")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "resource type is invalid")
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
u := &user{Id: "id", Address: "addr"}
|
||||
require.NoError(t, r.rw.Create(ctx, u))
|
||||
|
||||
r.rw.DoTx(ctx, 1, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
require.NoError(t, upsertRefreshToken(ctx, w, u, targetResourceType, "token"))
|
||||
return nil
|
||||
})
|
||||
got, err := r.lookupRefreshToken(ctx, u, targetResourceType)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
|
||||
assert.NoError(t, r.deleteRefreshToken(ctx, u, targetResourceType))
|
||||
|
||||
got, err = r.lookupRefreshToken(ctx, u, targetResourceType)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, got)
|
||||
})
|
||||
}
|
||||
+42
-16
@@ -15,30 +15,32 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/mql"
|
||||
)
|
||||
|
||||
// SessionRetrievalFunc is a function that retrieves sessions
|
||||
// from the provided boundary addr using the provided token.
|
||||
type SessionRetrievalFunc func(ctx context.Context, addr, token string) ([]*sessions.Session, error)
|
||||
type SessionRetrievalFunc func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) (ret []*sessions.Session, removedIds []string, refreshToken RefreshTokenValue, err error)
|
||||
|
||||
func defaultSessionFunc(ctx context.Context, addr, token string) ([]*sessions.Session, error) {
|
||||
func defaultSessionFunc(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*sessions.Session, []string, RefreshTokenValue, error) {
|
||||
const op = "cache.defaultSessionFunc"
|
||||
client, err := api.NewClient(&api.Config{
|
||||
Addr: addr,
|
||||
Token: token,
|
||||
Token: authTok,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
return nil, nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
sClient := sessions.NewClient(client)
|
||||
l, err := sClient.List(ctx, "global", sessions.WithRecursive(true))
|
||||
l, err := sClient.List(ctx, "global", sessions.WithRecursive(true), sessions.WithRefreshToken(string(refreshTok)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
return nil, nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return l.Items, nil
|
||||
return l.Items, l.RemovedIds, RefreshTokenValue(l.RefreshToken), nil
|
||||
}
|
||||
|
||||
func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
|
||||
@@ -51,6 +53,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
case u.Address == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user boundary address is missing")
|
||||
}
|
||||
const resourceType = sessionResourceType
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
@@ -59,16 +62,28 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
if opts.withSessionRetrievalFunc == nil {
|
||||
opts.withSessionRetrievalFunc = defaultSessionFunc
|
||||
}
|
||||
oldRefreshToken, err := r.lookupRefreshToken(ctx, u, resourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving sessions
|
||||
var gotResponse bool
|
||||
var resp []*sessions.Session
|
||||
var newRefreshToken RefreshTokenValue
|
||||
var removedIds []string
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, err = opts.withSessionRetrievalFunc(ctx, u.Address, t)
|
||||
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, oldRefreshToken)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
if err := r.deleteRefreshToken(ctx, u, resourceType); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// try again without the refresh token
|
||||
oldRefreshToken = ""
|
||||
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, "")
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: If we get an error about the token no longer having
|
||||
// permissions, remove it.
|
||||
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +92,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resource.Session.String(), retErr); saveErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -87,10 +102,17 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
|
||||
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d sessions for user %v", len(resp), u))
|
||||
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
// TODO: Instead of deleting everything, use refresh tokens and apply the delta
|
||||
if _, err := w.Exec(ctx, "delete from session where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
switch {
|
||||
case oldRefreshToken == "":
|
||||
if _, err := w.Exec(ctx, "delete from session where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
}
|
||||
case len(removedIds) > 0:
|
||||
if _, err := w.Exec(ctx, "delete from session where id in @ids",
|
||||
[]any{sql.Named("ids", removedIds)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range resp {
|
||||
@@ -114,6 +136,10 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+111
-4
@@ -116,13 +116,22 @@ func TestRepository_refreshSessions(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.refreshSessions(ctx, tc.u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(tc.sess)))
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{tc.sess}, [][]string{nil})))
|
||||
if tc.errorContains == "" {
|
||||
assert.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
var got []*Session
|
||||
require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil))
|
||||
assert.Len(t, got, tc.wantCount)
|
||||
|
||||
t.Cleanup(func() {
|
||||
refTok := &refreshToken{
|
||||
UserId: tc.u.Id,
|
||||
ResourceType: sessionResourceType,
|
||||
}
|
||||
_, err := r.rw.Delete(ctx, refTok)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
@@ -130,6 +139,96 @@ func TestRepository_refreshSessions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_RefreshSessions_withRefreshTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
u := user{
|
||||
Id: "u1",
|
||||
Address: addr,
|
||||
}
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: u.Id,
|
||||
}
|
||||
kt := KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "token",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt.KeyringType, kt.TokenName}: at,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
|
||||
ss := [][]*sessions.Session{
|
||||
{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Status: "status1",
|
||||
Endpoint: "address1",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Status: "status2",
|
||||
Endpoint: "address2",
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Status: "status3",
|
||||
Endpoint: "address3",
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = r.refreshSessions(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil})))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err := r.ListSessions(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 2)
|
||||
|
||||
// Refreshing again uses the refresh token and get additional sessions, appending
|
||||
// them to the response
|
||||
err = r.refreshSessions(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil})))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListSessions(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 3)
|
||||
|
||||
// Refreshing again wont return any more resources, but also none should be
|
||||
// removed
|
||||
require.NoError(t, r.refreshSessions(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil}))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListSessions(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 3)
|
||||
|
||||
// Refresh again with the refresh token being reported as invalid.
|
||||
require.NoError(t, r.refreshSessions(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testErroringForRefreshTokenRetrievalFunc(t, ss[0]))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListSessions(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 2)
|
||||
}
|
||||
|
||||
func TestRepository_ListSessions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
@@ -200,7 +299,7 @@ func TestRepository_ListSessions(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(ss))))
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil}))))
|
||||
|
||||
t.Run("wrong user gets no sessions", func(t *testing.T) {
|
||||
l, err := r.ListSessions(ctx, kt2.AuthTokenId)
|
||||
@@ -307,7 +406,7 @@ func TestRepository_QuerySessions(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(ss))))
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil}))))
|
||||
|
||||
t.Run("wrong token gets no sessions", func(t *testing.T) {
|
||||
l, err := r.QuerySessions(ctx, kt2.AuthTokenId, query)
|
||||
@@ -337,7 +436,15 @@ func TestDefaultSessionRetrievalFunc(t *testing.T) {
|
||||
_, err = tarClient.AuthorizeSession(tc.Context(), tar1.Item.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err := defaultSessionFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token)
|
||||
got, removed, refTok, err := defaultSessionFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, refTok)
|
||||
assert.Empty(t, removed)
|
||||
assert.Len(t, got, 1)
|
||||
|
||||
got2, removed2, refTok2, err := defaultSessionFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, refTok)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, refTok2)
|
||||
assert.Empty(t, removed2)
|
||||
assert.Empty(t, got2)
|
||||
}
|
||||
+44
-16
@@ -15,30 +15,32 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/mql"
|
||||
)
|
||||
|
||||
// TargetRetrievalFunc is a function that retrieves targets
|
||||
// from the provided boundary addr using the provided token.
|
||||
type TargetRetrievalFunc func(ctx context.Context, addr, token string) ([]*targets.Target, error)
|
||||
type TargetRetrievalFunc func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) (ret []*targets.Target, removedIds []string, refreshToken RefreshTokenValue, err error)
|
||||
|
||||
func defaultTargetFunc(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
||||
func defaultTargetFunc(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*targets.Target, []string, RefreshTokenValue, error) {
|
||||
const op = "cache.defaultTargetFunc"
|
||||
client, err := api.NewClient(&api.Config{
|
||||
Addr: addr,
|
||||
Token: token,
|
||||
Token: authTok,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
return nil, nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
tarClient := targets.NewClient(client)
|
||||
l, err := tarClient.List(ctx, "global", targets.WithRecursive(true))
|
||||
l, err := tarClient.List(ctx, "global", targets.WithRecursive(true), targets.WithRefreshToken(string(refreshTok)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
return nil, nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return l.Items, nil
|
||||
return l.Items, l.RemovedIds, RefreshTokenValue(l.RefreshToken), nil
|
||||
}
|
||||
|
||||
func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
|
||||
@@ -49,6 +51,7 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
case u.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id is missing")
|
||||
}
|
||||
const resourceType = targetResourceType
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
@@ -57,16 +60,28 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
if opts.withTargetRetrievalFunc == nil {
|
||||
opts.withTargetRetrievalFunc = defaultTargetFunc
|
||||
}
|
||||
oldRefreshToken, err := r.lookupRefreshToken(ctx, u, resourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving targets
|
||||
var gotResponse bool
|
||||
var resp []*targets.Target
|
||||
var removedIds []string
|
||||
var newRefreshToken RefreshTokenValue
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, err = opts.withTargetRetrievalFunc(ctx, u.Address, t)
|
||||
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, oldRefreshToken)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
if err := r.deleteRefreshToken(ctx, u, resourceType); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// try again without the refresh token
|
||||
oldRefreshToken = ""
|
||||
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, "")
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: If we get an error about the token no longer having
|
||||
// permissions, remove it.
|
||||
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
|
||||
continue
|
||||
}
|
||||
@@ -74,7 +89,7 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
break
|
||||
}
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resource.Target.String(), retErr); saveErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -84,10 +99,17 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
|
||||
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d targets for user %v", len(resp), u))
|
||||
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
// TODO: Instead of deleting everything, use refresh tokens and apply the delta
|
||||
if _, err := w.Exec(ctx, "delete from target where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
switch {
|
||||
case oldRefreshToken == "":
|
||||
if _, err := w.Exec(ctx, "delete from target where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
}
|
||||
case len(removedIds) > 0:
|
||||
if _, err := w.Exec(ctx, "delete from target where id in @ids",
|
||||
[]any{sql.Named("ids", removedIds)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range resp {
|
||||
@@ -111,6 +133,12 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newRefreshToken != "" || oldRefreshToken != "" {
|
||||
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+112
-4
@@ -110,13 +110,22 @@ func TestRepository_refreshTargets(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.refreshTargets(ctx, tc.u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(tc.targets)))
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{tc.targets}, [][]string{nil})))
|
||||
if tc.errorContains == "" {
|
||||
assert.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
var got []*Target
|
||||
require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil))
|
||||
assert.Len(t, got, tc.wantCount)
|
||||
|
||||
t.Cleanup(func() {
|
||||
refTok := &refreshToken{
|
||||
UserId: tc.u.Id,
|
||||
ResourceType: targetResourceType,
|
||||
}
|
||||
_, err := r.rw.Delete(ctx, refTok)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
@@ -124,6 +133,97 @@ func TestRepository_refreshTargets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_RefreshTargets_withRefreshTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
u := user{
|
||||
Id: "u1",
|
||||
Address: addr,
|
||||
}
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: u.Id,
|
||||
}
|
||||
kt := KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "token",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt.KeyringType, kt.TokenName}: at,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
|
||||
ts := [][]*targets.Target{
|
||||
{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Name: "name1",
|
||||
Address: "address1",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 111,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Name: "name2",
|
||||
Address: "address2",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 222,
|
||||
},
|
||||
}, {
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Name: "name3",
|
||||
Address: "address3",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 333,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, r.refreshTargets(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, ts, [][]string{nil, nil}))))
|
||||
|
||||
got, err := r.ListTargets(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 2)
|
||||
|
||||
// Refreshing again uses the refresh token and get additional sessions, appending
|
||||
// them to the response
|
||||
require.NoError(t, r.refreshTargets(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, ts, [][]string{nil, nil}))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListTargets(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 3)
|
||||
|
||||
// Refreshing again wont return any more resources, but also none should be
|
||||
// removed
|
||||
require.NoError(t, r.refreshTargets(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, ts, [][]string{nil, nil}))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListTargets(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 3)
|
||||
|
||||
// Refresh again with the refresh token being reported as invalid.
|
||||
require.NoError(t, r.refreshTargets(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testErroringForRefreshTokenRetrievalFunc(t, ts[0]))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err = r.ListTargets(ctx, at.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 2)
|
||||
}
|
||||
|
||||
func TestRepository_ListTargets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
@@ -190,7 +290,7 @@ func TestRepository_ListTargets(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(ts))))
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{ts}, [][]string{nil}))))
|
||||
|
||||
t.Run("wrong user gets no targets", func(t *testing.T) {
|
||||
l, err := r.ListTargets(ctx, kt2.AuthTokenId)
|
||||
@@ -294,7 +394,7 @@ func TestRepository_QueryTargets(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(ts))))
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{ts}, [][]string{nil}))))
|
||||
|
||||
t.Run("wrong token gets no targets", func(t *testing.T) {
|
||||
l, err := r.QueryTargets(ctx, kt2.AuthTokenId, query)
|
||||
@@ -321,8 +421,16 @@ func TestDefaultTargetRetrievalFunc(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tar2)
|
||||
|
||||
got, err := defaultTargetFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token)
|
||||
got, removed, refTok, err := defaultTargetFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, refTok)
|
||||
assert.Empty(t, removed)
|
||||
assert.Contains(t, got, tar1.Item)
|
||||
assert.Contains(t, got, tar2.Item)
|
||||
|
||||
got2, removed2, refTok2, err := defaultTargetFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, refTok)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, refTok2)
|
||||
assert.Empty(t, removed2)
|
||||
assert.Empty(t, got2)
|
||||
}
|
||||
+1
-1
@@ -54,7 +54,7 @@ func TestRepository_SaveError(t *testing.T) {
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
testResource := "test_resource_type"
|
||||
testResource := targetResourceType
|
||||
testErr := fmt.Errorf("test error for %q", testResource)
|
||||
|
||||
u := &user{
|
||||
|
||||
+104
@@ -135,6 +135,110 @@ func TestAuthToken_NoMoreKeyringTokens(t *testing.T) {
|
||||
assert.NoError(t, rw.LookupById(ctx, u))
|
||||
}
|
||||
|
||||
func TestRefreshToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw := db.New(s)
|
||||
|
||||
u := &user{
|
||||
Id: "userId",
|
||||
Address: "address",
|
||||
}
|
||||
|
||||
t.Run("no user foreign key constraint", func(t *testing.T) {
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "something",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
t.Run("no user id", func(t *testing.T) {
|
||||
tok := &refreshToken{
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "something",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("unknown resource type", func(t *testing.T) {
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: "thisisntknown",
|
||||
RefreshToken: "something",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("empty refresh token", func(t *testing.T) {
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: "thisisntknown",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "something",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
require.NoError(t, rw.LookupById(ctx, tok))
|
||||
assert.NotEmpty(t, tok.RefreshToken)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "updatethis",
|
||||
Address: "updated",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "started",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
|
||||
tok.RefreshToken = "updated"
|
||||
n, err := rw.Update(ctx, tok, []string{"RefreshToken"}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
})
|
||||
|
||||
t.Run("delete user deletes token", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "deletethis",
|
||||
Address: "deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
tok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "deleted_soon",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
|
||||
_, err = rw.Exec(ctx, "delete from user where id = ?", []any{u.Id})
|
||||
|
||||
require.True(t, errors.IsNotFoundError(rw.LookupById(ctx, tok)))
|
||||
})
|
||||
|
||||
// TODO: When gorm sqlite driver fixes it's delete, use rw.Delete instead of the Exec.
|
||||
// n, err := rw.Delete(ctx, p)
|
||||
_, err = rw.Exec(ctx, "delete from refresh_token", nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
|
||||
Reference in new issue
Block a user