mirror of https://github.com/hashicorp/boundary
Store and use refresh tokens in the client cache (#3857)
* Refresh resources using refresh tokenspull/4202/head
parent
5daef58f54
commit
b2fec2e4f0
@ -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)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// The refreshtoken package encapsulates domain logic surrounding
|
||||
// list endpoint refresh tokens. Refresh tokens are used when users
|
||||
// paginate through results in our list endpoints, and also to
|
||||
// allow users to request new, updated and deleted resources.
|
||||
package refreshtoken
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/boundary"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
)
|
||||
|
||||
// A Token is returned in list endpoints for the purposes of pagination
|
||||
type Token struct {
|
||||
CreatedTime time.Time
|
||||
UpdatedTime time.Time
|
||||
ResourceType resource.Type
|
||||
GrantsHash []byte
|
||||
LastItemId string
|
||||
LastItemUpdatedTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new refresh token from a createdTime, resource type, grants hash, and last item information
|
||||
func New(ctx context.Context, createdTime time.Time, updatedTime time.Time, typ resource.Type, grantsHash []byte, lastItemId string, lastItemUpdatedTime time.Time) (*Token, error) {
|
||||
const op = "refreshtoken.New"
|
||||
|
||||
if len(grantsHash) == 0 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
}
|
||||
if createdTime.After(time.Now()) {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "created time is in the future")
|
||||
}
|
||||
if createdTime.Before(time.Now().AddDate(0, 0, -30)) {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "created time is too old")
|
||||
}
|
||||
if updatedTime.Before(createdTime) {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "updated time is older than created time")
|
||||
}
|
||||
if updatedTime.After(time.Now()) {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "updated time is in the future")
|
||||
}
|
||||
if lastItemId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing last item ID")
|
||||
}
|
||||
if lastItemUpdatedTime.After(time.Now()) {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "last item updated time is in the future")
|
||||
}
|
||||
|
||||
return &Token{
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: updatedTime,
|
||||
ResourceType: typ,
|
||||
GrantsHash: grantsHash,
|
||||
LastItemId: lastItemId,
|
||||
LastItemUpdatedTime: lastItemUpdatedTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FromResource creates a new refresh token from a resource and grants hash
|
||||
func FromResource(res boundary.Resource, grantsHash []byte) *Token {
|
||||
t := time.Now()
|
||||
return &Token{
|
||||
CreatedTime: t,
|
||||
UpdatedTime: t,
|
||||
ResourceType: res.GetResourceType(),
|
||||
GrantsHash: grantsHash,
|
||||
LastItemId: res.GetPublicId(),
|
||||
LastItemUpdatedTime: res.GetUpdateTime().AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh refreshes a token's updated time
|
||||
func (rt *Token) Refresh(updatedTime time.Time) *Token {
|
||||
rt.UpdatedTime = updatedTime
|
||||
return rt
|
||||
}
|
||||
|
||||
// RefreshLastItem refreshes a token's updated time and last item
|
||||
func (rt *Token) RefreshLastItem(res boundary.Resource, updatedTime time.Time) *Token {
|
||||
rt.UpdatedTime = updatedTime
|
||||
rt.LastItemId = res.GetPublicId()
|
||||
rt.LastItemUpdatedTime = res.GetUpdateTime().AsTime()
|
||||
return rt
|
||||
}
|
||||
|
||||
// Validate validates the refresh token.
|
||||
func (rt *Token) Validate(
|
||||
ctx context.Context,
|
||||
expectedResourceType resource.Type,
|
||||
expectedGrantsHash []byte,
|
||||
) error {
|
||||
const op = "refreshtoken.Validate"
|
||||
if rt == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token was missing")
|
||||
}
|
||||
if len(rt.GrantsHash) == 0 {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token was missing its grants hash")
|
||||
}
|
||||
if !bytes.Equal(rt.GrantsHash, expectedGrantsHash) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "grants have changed since refresh token was issued")
|
||||
}
|
||||
if rt.CreatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token was created in the future")
|
||||
}
|
||||
// Tokens older than 30 days have expired
|
||||
if rt.CreatedTime.Before(time.Now().AddDate(0, 0, -30)) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token was expired")
|
||||
}
|
||||
if rt.UpdatedTime.Before(rt.CreatedTime) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token was updated before its creation time")
|
||||
}
|
||||
if rt.UpdatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token was updated in the future")
|
||||
}
|
||||
if rt.LastItemId == "" {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token missing last item ID")
|
||||
}
|
||||
if rt.LastItemUpdatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token last item was updated in the future")
|
||||
}
|
||||
if rt.ResourceType != expectedResourceType {
|
||||
return errors.New(ctx, errors.InvalidRefreshToken, op, "refresh token resource type does not match expected resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,398 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package refreshtoken_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/internal/boundary"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/refreshtoken"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ValidateRefreshToken(t *testing.T) {
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
tests := []struct {
|
||||
name string
|
||||
token *refreshtoken.Token
|
||||
grantsHash []byte
|
||||
resourceType resource.Type
|
||||
wantErrString string
|
||||
wantErrCode errors.Code
|
||||
}{
|
||||
{
|
||||
name: "valid token",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
},
|
||||
{
|
||||
name: "nil token",
|
||||
token: nil,
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was missing",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "no grants hash",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: nil,
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was missing its grants hash",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "changed grants hash",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some other hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "grants have changed since refresh token was issued",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "created in the future",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: time.Now().AddDate(1, 0, 0),
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was created in the future",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: time.Now().AddDate(0, 0, -31),
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was expired",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "updated before created",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, -1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was updated before its creation time",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "updated after now",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: time.Now().AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token was updated in the future",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "resource type mismatch",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.SessionRecording,
|
||||
wantErrString: "refresh token resource type does not match expected resource type",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "last item ID unset",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token missing last item ID",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "last item ID unset",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token missing last item ID",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
{
|
||||
name: "updated in the future",
|
||||
token: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "s_1234567890",
|
||||
LastItemUpdatedTime: time.Now().AddDate(1, 0, 0),
|
||||
},
|
||||
grantsHash: []byte("some hash"),
|
||||
resourceType: resource.Target,
|
||||
wantErrString: "refresh token last item was updated in the future",
|
||||
wantErrCode: errors.InvalidRefreshToken,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.token.Validate(context.Background(), tt.resourceType, tt.grantsHash)
|
||||
if tt.wantErrString != "" {
|
||||
require.ErrorContains(t, err, tt.wantErrString)
|
||||
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
tests := []struct {
|
||||
name string
|
||||
createdTime time.Time
|
||||
updatedTime time.Time
|
||||
typ resource.Type
|
||||
grantsHash []byte
|
||||
lastItemId string
|
||||
lastItemUpdatedTime time.Time
|
||||
want *refreshtoken.Token
|
||||
wantErrString string
|
||||
wantErrCode errors.Code
|
||||
}{
|
||||
{
|
||||
name: "valid refresh token",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
want: &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "some id",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing grants hash",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: nil,
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "missing grants hash",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "new created time",
|
||||
createdTime: fiveDaysAgo.AddDate(1, 0, 0),
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "created time is in the future",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "old created time",
|
||||
createdTime: fiveDaysAgo.AddDate(-1, 0, 0),
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "created time is too old",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "new updated time",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(1, 0, 0),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "updated time is in the future",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "updated time older than created time",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, -11),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "updated time is older than created time",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "missing last item id",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "",
|
||||
lastItemUpdatedTime: fiveDaysAgo,
|
||||
wantErrString: "missing last item ID",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "new last item updated time",
|
||||
createdTime: fiveDaysAgo,
|
||||
updatedTime: fiveDaysAgo.AddDate(0, 0, 1),
|
||||
typ: resource.Target,
|
||||
grantsHash: []byte("some hash"),
|
||||
lastItemId: "some id",
|
||||
lastItemUpdatedTime: fiveDaysAgo.AddDate(1, 0, 0),
|
||||
wantErrString: "last item updated time is in the future",
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := refreshtoken.New(context.Background(), tt.createdTime, tt.updatedTime, tt.typ, tt.grantsHash, tt.lastItemId, tt.lastItemUpdatedTime)
|
||||
if tt.wantErrString != "" {
|
||||
require.ErrorContains(t, err, tt.wantErrString)
|
||||
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(got, tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTargetResource struct {
|
||||
boundary.Resource
|
||||
|
||||
publicId string
|
||||
updateTime *timestamp.Timestamp
|
||||
}
|
||||
|
||||
func (m *fakeTargetResource) GetResourceType() resource.Type {
|
||||
return resource.Target
|
||||
}
|
||||
|
||||
func (m *fakeTargetResource) GetPublicId() string {
|
||||
return m.publicId
|
||||
}
|
||||
|
||||
func (m *fakeTargetResource) GetUpdateTime() *timestamp.Timestamp {
|
||||
return m.updateTime
|
||||
}
|
||||
|
||||
func TestFromResource(t *testing.T) {
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
res := &fakeTargetResource{
|
||||
publicId: "tcp_1234567890",
|
||||
updateTime: timestamp.New(fiveDaysAgo),
|
||||
}
|
||||
|
||||
tok := refreshtoken.FromResource(res, []byte("some hash"))
|
||||
|
||||
// Check that it's within 1 second of now according to the system
|
||||
// If this is flaky... just increase the limit 😬.
|
||||
require.True(t, tok.CreatedTime.Before(time.Now().Add(time.Second)))
|
||||
require.True(t, tok.CreatedTime.After(time.Now().Add(-time.Second)))
|
||||
require.True(t, tok.UpdatedTime.Before(time.Now().Add(time.Second)))
|
||||
require.True(t, tok.UpdatedTime.After(time.Now().Add(-time.Second)))
|
||||
|
||||
require.Equal(t, tok.ResourceType, res.GetResourceType())
|
||||
require.Equal(t, tok.GrantsHash, []byte("some hash"))
|
||||
require.Equal(t, tok.LastItemId, res.GetPublicId())
|
||||
require.True(t, tok.LastItemUpdatedTime.Equal(res.GetUpdateTime().AsTime()))
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
createdTime := time.Now().AddDate(0, 0, -5)
|
||||
updatedTime := time.Now()
|
||||
tok := &refreshtoken.Token{
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: createdTime,
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "tcp_1234567890",
|
||||
LastItemUpdatedTime: createdTime,
|
||||
}
|
||||
newTok := tok.Refresh(updatedTime)
|
||||
|
||||
require.True(t, newTok.UpdatedTime.Equal(updatedTime))
|
||||
require.True(t, newTok.CreatedTime.Equal(createdTime))
|
||||
require.Equal(t, newTok.ResourceType, tok.ResourceType)
|
||||
require.Equal(t, newTok.GrantsHash, tok.GrantsHash)
|
||||
require.Equal(t, newTok.LastItemId, tok.LastItemId)
|
||||
require.True(t, newTok.LastItemUpdatedTime.Equal(tok.LastItemUpdatedTime))
|
||||
}
|
||||
Loading…
Reference in new issue