mirror of https://github.com/hashicorp/boundary
The listtoken package encapsulates the logic used for all types of pagination and refreshing across all list endpoints.pull/4202/head
parent
8b0d1aa4af
commit
3019038eab
@ -0,0 +1,60 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package listtoken
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
)
|
||||
|
||||
// Item represents a generic resource with a public ID,
|
||||
// create time, update time and resource type.
|
||||
type Item struct {
|
||||
publicId string
|
||||
// Only one of these two is ever set at a time.
|
||||
// In the case of a pagination token item, it will
|
||||
// be the create time. In the case of a refresh token,
|
||||
// it's the update time.
|
||||
createTime *timestamp.Timestamp
|
||||
updateTime *timestamp.Timestamp
|
||||
resourceType resource.Type
|
||||
}
|
||||
|
||||
// GetPublicId gets the public ID of the item.
|
||||
func (p *Item) GetPublicId() string {
|
||||
return p.publicId
|
||||
}
|
||||
|
||||
// GetCreateTime gets the create time of the item.
|
||||
func (p *Item) GetCreateTime() *timestamp.Timestamp {
|
||||
return p.createTime
|
||||
}
|
||||
|
||||
// GetUpdateTime gets the update time of the item.
|
||||
func (p *Item) GetUpdateTime() *timestamp.Timestamp {
|
||||
return p.updateTime
|
||||
}
|
||||
|
||||
// GetResourceType gets the resource type of the item.
|
||||
func (p *Item) GetResourceType() resource.Type {
|
||||
return p.resourceType
|
||||
}
|
||||
|
||||
// Validate can be called to validate that an Item
|
||||
// is valid.
|
||||
func (p *Item) Validate() error {
|
||||
switch {
|
||||
case p.publicId == "":
|
||||
return errors.New("missing public id")
|
||||
case p.resourceType == resource.Unknown:
|
||||
return errors.New("missing resource type")
|
||||
case p.createTime != nil && p.updateTime != nil:
|
||||
return errors.New("both create time and update time is set")
|
||||
case p.createTime == nil && p.updateTime == nil:
|
||||
return errors.New("neither create time nor update time is set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package listtoken_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/listtoken"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestItem_Validate(t *testing.T) {
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
t.Run("valid-pagination-item", func(t *testing.T) {
|
||||
tk, err := listtoken.NewPagination(
|
||||
context.Background(),
|
||||
fiveDaysAgo,
|
||||
resource.Target,
|
||||
[]byte("some hash"),
|
||||
"some id",
|
||||
fiveDaysAgo.Add(time.Hour),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
item, err := tk.LastItem(context.Background())
|
||||
require.NoError(t, err)
|
||||
err = item.Validate()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("valid-refresh-item", func(t *testing.T) {
|
||||
tk, err := listtoken.NewRefresh(
|
||||
context.Background(),
|
||||
fiveDaysAgo,
|
||||
resource.Target,
|
||||
[]byte("some hash"),
|
||||
fiveDaysAgo.Add(4*time.Hour),
|
||||
fiveDaysAgo.Add(3*time.Hour),
|
||||
fiveDaysAgo.Add(2*time.Hour),
|
||||
"some id",
|
||||
fiveDaysAgo.Add(time.Hour),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
item, err := tk.LastItem(context.Background())
|
||||
require.NoError(t, err)
|
||||
err = item.Validate()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("invalid-item", func(t *testing.T) {
|
||||
item := &listtoken.Item{}
|
||||
err := item.Validate()
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,414 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Package listtoken encapsulates domain logic surrounding
|
||||
// list endpoint tokens. List 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 listtoken
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/boundary"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"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.
|
||||
// A Token has a subtype, which defines which stage in the list pagination
|
||||
// lifecycle is in place. The transitions between subtypes can be seen as
|
||||
// a state machine with the following diagram:
|
||||
//
|
||||
// ,---------------------.
|
||||
// | Initial Request |
|
||||
// `---------------------'
|
||||
// * *
|
||||
// / | More pages in initial phase
|
||||
// / |
|
||||
// | ,---------------------.
|
||||
// | | PaginationToken | *-. More results in this page
|
||||
// | `---------------------' <-'
|
||||
// | *
|
||||
// | No more results | End of initial pagination phase
|
||||
// | |
|
||||
// ,----------------------.
|
||||
// | StartRefreshToken | *-. End of refresh phase
|
||||
// `----------------------' <-'
|
||||
// * ^
|
||||
// | More results | End of refresh phase
|
||||
// | *
|
||||
// ,--------------------.
|
||||
// | RefreshToken | *-. More result in this page
|
||||
// `--------------------' <-'
|
||||
//
|
||||
// For more information, please consult ICU-110
|
||||
type Token struct {
|
||||
// The create time of the token. Constant for the lifetime
|
||||
// of the token.
|
||||
CreateTime time.Time
|
||||
// The resource type of the list endpoint this token
|
||||
// is associated with. Constant for the lifetime
|
||||
// of the token.
|
||||
ResourceType resource.Type
|
||||
// A hash of the grants of the user who made the original
|
||||
// request. Only used to ensure that grants have not changed
|
||||
// between requests. Constant for the lifetime of
|
||||
// the token.
|
||||
GrantsHash []byte
|
||||
// The specific subtype of this token. Always
|
||||
// set ot either PaginationToken, StartRefreshToken
|
||||
// or RefreshToken.
|
||||
Subtype TokenSubtype
|
||||
}
|
||||
|
||||
// TokenSubtype is used to create a discriminated union of types
|
||||
// that can be used as a subtype for a list token.
|
||||
type TokenSubtype interface {
|
||||
isTokenSubtype()
|
||||
}
|
||||
|
||||
// Pagination token represents a pagination token subtype to a list
|
||||
// token. It is used during the initial pagination phase.
|
||||
type PaginationToken struct {
|
||||
// The ID of the last item on the previous page.
|
||||
LastItemId string
|
||||
// The create time of the last item on the previous page.
|
||||
LastItemCreateTime time.Time
|
||||
}
|
||||
|
||||
func (*PaginationToken) isTokenSubtype() {}
|
||||
|
||||
// StartRefreshToken represents the transition between two phases,
|
||||
// either the initial pagination phase and the first refresh phase,
|
||||
// or between refresh phases.
|
||||
type StartRefreshToken struct {
|
||||
// The end time of the phase previous to this one,
|
||||
// which should be used as the lower bound for the
|
||||
// new refresh phase.
|
||||
PreviousPhaseUpperBound time.Time
|
||||
// The timestamp of the transaction that last listed the deleted IDs,
|
||||
// for use as a lower bound in the next deleted IDs list.
|
||||
PreviousDeletedIdsTime time.Time
|
||||
}
|
||||
|
||||
func (*StartRefreshToken) isTokenSubtype() {}
|
||||
|
||||
// RefreshToken represents a refresh phase.
|
||||
type RefreshToken struct {
|
||||
// The upper bound for the timestamp comparisons in
|
||||
// this refresh phase. This is equal to the time that
|
||||
// the first request in this phase was processed.
|
||||
// Constant for the lifetime of the refresh phase.
|
||||
PhaseUpperBound time.Time
|
||||
// The lower bound for the timestamp comparisons in
|
||||
// this refresh phase. This is equal to the initial
|
||||
// create time of the token if the previous phase was
|
||||
// the initial pagination phase, or the upper bound of
|
||||
// the previous refresh phase otherwise.
|
||||
// Constant for the lifetime of the refresh phase.
|
||||
PhaseLowerBound time.Time
|
||||
// The timestamp of the transaction that last listed the deleted IDs,
|
||||
// for use as a lower bound in the next deleted IDs list.
|
||||
PreviousDeletedIdsTime time.Time
|
||||
// The ID of the last item on the previous page.
|
||||
LastItemId string
|
||||
// The update time of the last item on the previous page.
|
||||
LastItemUpdateTime time.Time
|
||||
}
|
||||
|
||||
func (*RefreshToken) isTokenSubtype() {}
|
||||
|
||||
// NewPagination creates a new token with the pagination subtype.
|
||||
func NewPagination(
|
||||
ctx context.Context,
|
||||
createTime time.Time,
|
||||
typ resource.Type,
|
||||
grantsHash []byte,
|
||||
lastItemId string,
|
||||
lastItemCreateTime time.Time,
|
||||
) (*Token, error) {
|
||||
const op = "listtoken.NewPagination"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case createTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is in the future")
|
||||
case createTime.Before(time.Now().AddDate(0, 0, -30)):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is too old")
|
||||
case lastItemId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing last item ID")
|
||||
case lastItemCreateTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "last item create time is in the future")
|
||||
}
|
||||
|
||||
return &Token{
|
||||
CreateTime: createTime,
|
||||
ResourceType: typ,
|
||||
GrantsHash: grantsHash,
|
||||
Subtype: &PaginationToken{
|
||||
LastItemId: lastItemId,
|
||||
LastItemCreateTime: lastItemCreateTime,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewStartRefresh creates a new token with a start-refresh subtype.
|
||||
func NewStartRefresh(
|
||||
ctx context.Context,
|
||||
createTime time.Time,
|
||||
typ resource.Type,
|
||||
grantsHash []byte,
|
||||
previousDeletedIdsTime time.Time,
|
||||
previousPhaseUpperBound time.Time,
|
||||
) (*Token, error) {
|
||||
const op = "listtoken.NewStartRefresh"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case createTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is in the future")
|
||||
case createTime.Before(time.Now().AddDate(0, 0, -30)):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is too old")
|
||||
case previousDeletedIdsTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "previous deleted ids time is in the future")
|
||||
case previousPhaseUpperBound.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "previous phase upper bound is in the future")
|
||||
}
|
||||
|
||||
return &Token{
|
||||
CreateTime: createTime,
|
||||
ResourceType: typ,
|
||||
GrantsHash: grantsHash,
|
||||
Subtype: &StartRefreshToken{
|
||||
PreviousPhaseUpperBound: previousPhaseUpperBound,
|
||||
PreviousDeletedIdsTime: previousDeletedIdsTime,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewRefresh creates a new token with a refresh subtype.
|
||||
func NewRefresh(
|
||||
ctx context.Context,
|
||||
createTime time.Time,
|
||||
typ resource.Type,
|
||||
grantsHash []byte,
|
||||
previousDeletedIdsTime time.Time,
|
||||
phaseUpperBound time.Time,
|
||||
phaseLowerBound time.Time,
|
||||
lastItemId string,
|
||||
lastItemUpdateTime time.Time,
|
||||
) (*Token, error) {
|
||||
const op = "listtoken.NewRefresh"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case createTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is in the future")
|
||||
case createTime.Before(time.Now().AddDate(0, 0, -30)):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "create time is too old")
|
||||
case previousDeletedIdsTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "previous deleted ids time is in the future")
|
||||
case phaseUpperBound.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "phase upper bound is in the future")
|
||||
case phaseLowerBound.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "phase lower bound is in the future")
|
||||
case phaseLowerBound.After(phaseUpperBound):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "phase lower bound is after phase upper bound")
|
||||
case lastItemId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing last item ID")
|
||||
case lastItemUpdateTime.After(time.Now()):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "last item update time is in the future")
|
||||
}
|
||||
|
||||
return &Token{
|
||||
CreateTime: createTime,
|
||||
ResourceType: typ,
|
||||
GrantsHash: grantsHash,
|
||||
Subtype: &RefreshToken{
|
||||
PhaseUpperBound: phaseUpperBound,
|
||||
PhaseLowerBound: phaseLowerBound,
|
||||
PreviousDeletedIdsTime: previousDeletedIdsTime,
|
||||
LastItemId: lastItemId,
|
||||
LastItemUpdateTime: lastItemUpdateTime,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LastItem returns the last item stored in the token.
|
||||
// This will differ depending on whether the token has
|
||||
// a pagination, start-refresh or refresh subtype.
|
||||
func (tk *Token) LastItem(ctx context.Context) (*Item, error) {
|
||||
const op = "listtoken.(*Token).LastItem"
|
||||
switch st := tk.Subtype.(type) {
|
||||
case *PaginationToken:
|
||||
return &Item{
|
||||
publicId: st.LastItemId,
|
||||
createTime: timestamp.New(st.LastItemCreateTime),
|
||||
resourceType: tk.ResourceType,
|
||||
}, nil
|
||||
case *RefreshToken:
|
||||
return &Item{
|
||||
publicId: st.LastItemId,
|
||||
updateTime: timestamp.New(st.LastItemUpdateTime),
|
||||
resourceType: tk.ResourceType,
|
||||
}, nil
|
||||
case *StartRefreshToken:
|
||||
// No item available when starting a new refresh phase.
|
||||
return nil, errors.New(ctx, errors.Internal, op, "start refresh tokens have no last item")
|
||||
default:
|
||||
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unexpected token subtype: %T", st))
|
||||
}
|
||||
}
|
||||
|
||||
// Transition transitions the token to the next state
|
||||
// in the state machine. See the documentation for the
|
||||
// [Token] type for an overview of the state machine.
|
||||
func (tk *Token) Transition(
|
||||
ctx context.Context,
|
||||
completeListing bool,
|
||||
lastItem boundary.Resource,
|
||||
deletedIdsTime time.Time,
|
||||
listTime time.Time,
|
||||
) error {
|
||||
const op = "listtoken.(*Token).Transition"
|
||||
switch st := (tk.Subtype).(type) {
|
||||
case *PaginationToken:
|
||||
if completeListing {
|
||||
// If this is the last page in the pagination, create a
|
||||
// start refresh token so subsequent requests are informed
|
||||
// that they need to start a new refresh phase.
|
||||
tk.Subtype = &StartRefreshToken{
|
||||
// In the next refresh phase, both deleted
|
||||
// ids and the items listing is relative
|
||||
// to the create time of this token.
|
||||
PreviousDeletedIdsTime: tk.CreateTime,
|
||||
PreviousPhaseUpperBound: tk.CreateTime,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Note: this is not a complete listing, which implies that
|
||||
// lastItem is populated.
|
||||
tk.Subtype = &PaginationToken{
|
||||
LastItemId: lastItem.GetPublicId(),
|
||||
LastItemCreateTime: lastItem.GetCreateTime().AsTime(),
|
||||
}
|
||||
case *StartRefreshToken:
|
||||
if completeListing {
|
||||
// If this is the only page in the pagination, create a
|
||||
// start refresh token so subsequent requests are informed
|
||||
// that they need to start a new refresh phase.
|
||||
tk.Subtype = &StartRefreshToken{
|
||||
PreviousDeletedIdsTime: deletedIdsTime,
|
||||
PreviousPhaseUpperBound: listTime,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
tk.Subtype = &RefreshToken{
|
||||
PhaseUpperBound: listTime,
|
||||
PhaseLowerBound: st.PreviousPhaseUpperBound,
|
||||
PreviousDeletedIdsTime: deletedIdsTime,
|
||||
LastItemId: lastItem.GetPublicId(),
|
||||
LastItemUpdateTime: lastItem.GetUpdateTime().AsTime(),
|
||||
}
|
||||
case *RefreshToken:
|
||||
if completeListing {
|
||||
// If this is the only page in the pagination, create a
|
||||
// start refresh token so subsequent requests are informed
|
||||
// that they need to start a new refresh phase.
|
||||
tk.Subtype = &StartRefreshToken{
|
||||
PreviousDeletedIdsTime: deletedIdsTime,
|
||||
PreviousPhaseUpperBound: st.PhaseUpperBound,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Note: this is not a complete listing, which implies that
|
||||
// lastItem is populated.
|
||||
tk.Subtype = &RefreshToken{
|
||||
PhaseUpperBound: st.PhaseUpperBound,
|
||||
PhaseLowerBound: st.PhaseLowerBound,
|
||||
PreviousDeletedIdsTime: deletedIdsTime,
|
||||
LastItemId: lastItem.GetPublicId(),
|
||||
LastItemUpdateTime: lastItem.GetUpdateTime().AsTime(),
|
||||
}
|
||||
default:
|
||||
return errors.New(ctx, errors.Internal, op, fmt.Sprintf("unexpected token subtype: %T", st))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the contents of the token.
|
||||
func (tk *Token) Validate(
|
||||
ctx context.Context,
|
||||
expectedResourceType resource.Type,
|
||||
expectedGrantsHash []byte,
|
||||
) error {
|
||||
const op = "listtoken.Validate"
|
||||
switch {
|
||||
case tk == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token was missing")
|
||||
case len(tk.GrantsHash) == 0:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token was missing its grants hash")
|
||||
case !bytes.Equal(tk.GrantsHash, expectedGrantsHash):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "grants have changed since list token was issued")
|
||||
case tk.CreateTime.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token was created in the future")
|
||||
// Tokens older than 30 days have expired
|
||||
case tk.CreateTime.Before(time.Now().AddDate(0, 0, -30)):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token was expired")
|
||||
case tk.ResourceType != expectedResourceType:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token resource type does not match expected resource type")
|
||||
}
|
||||
switch st := tk.Subtype.(type) {
|
||||
case *RefreshToken:
|
||||
switch {
|
||||
case st.PhaseUpperBound.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's phase upper bound was before its creation time")
|
||||
case st.PhaseUpperBound.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's phase upper bound was in the future")
|
||||
case st.PhaseLowerBound.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's phase lower bound was before its creation time")
|
||||
case st.PhaseLowerBound.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's phase lower bound was in the future")
|
||||
case st.PhaseUpperBound.Before(st.PhaseLowerBound):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's phase upper bound was before the phase lower bound")
|
||||
case st.PreviousDeletedIdsTime.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component previous deleted ids time was before its creation time")
|
||||
case st.PreviousDeletedIdsTime.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component previous deleted ids time was in the future")
|
||||
case st.LastItemId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component missing last item ID")
|
||||
case st.LastItemUpdateTime.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's last item was updated in the future")
|
||||
case st.LastItemUpdateTime.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's refresh component's last item was updated before the list token's creation time")
|
||||
}
|
||||
case *PaginationToken:
|
||||
switch {
|
||||
case st.LastItemId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list tokens's pagination component missing last item ID")
|
||||
case st.LastItemCreateTime.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's pagination component's last item was created in the future")
|
||||
}
|
||||
case *StartRefreshToken:
|
||||
switch {
|
||||
case st.PreviousPhaseUpperBound.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's start refresh component's previous phase upper bound was before its creation time")
|
||||
case st.PreviousPhaseUpperBound.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's start refresh component's previous phase upper bound was in the future")
|
||||
case st.PreviousDeletedIdsTime.Before(tk.CreateTime):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's start refresh component previous deleted ids time was before its creation time")
|
||||
case st.PreviousDeletedIdsTime.After(time.Now()):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "list token's start refresh component previous deleted ids time was in the future")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,34 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package refreshtoken
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
)
|
||||
|
||||
// Item represents a generic resource with a public ID, update time
|
||||
// and resource type.
|
||||
type Item struct {
|
||||
publicId string
|
||||
updateTime time.Time
|
||||
resourceType resource.Type
|
||||
}
|
||||
|
||||
// GetPublicId gets the public ID of the item.
|
||||
func (p *Item) GetPublicId() string {
|
||||
return p.publicId
|
||||
}
|
||||
|
||||
// GetUpdateTime gets the update time of the item.
|
||||
func (p *Item) GetUpdateTime() *timestamp.Timestamp {
|
||||
return timestamp.New(p.updateTime)
|
||||
}
|
||||
|
||||
// GetResourceType gets the resource type of the item.
|
||||
func (p *Item) GetResourceType() resource.Type {
|
||||
return p.resourceType
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package refreshtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRefreshToken_ToPartialResource(t *testing.T) {
|
||||
ctime := time.Now().AddDate(0, 0, -1)
|
||||
utime := ctime.Add(time.Hour)
|
||||
rt, err := New(context.Background(), ctime, utime, resource.Session, []byte("some-hash"), "some-id", utime)
|
||||
require.NoError(t, err)
|
||||
res := rt.LastItem()
|
||||
assert.Equal(t, res.GetPublicId(), "some-id")
|
||||
assert.Equal(t, res.GetResourceType(), resource.Session)
|
||||
assert.Equal(t, res.GetUpdateTime(), timestamp.New(utime))
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Package refreshtoken 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"
|
||||
)
|
||||
|
||||
// UpdatedTimeBuffer is used to automatically adjust the updated
|
||||
// time of a refresh token to account for delays between overlapping
|
||||
// database transactions.
|
||||
const UpdatedTimeBuffer = 30 * time.Second
|
||||
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
// LastItem returns the last item stored in the token.
|
||||
func (rt *Token) LastItem() *Item {
|
||||
return &Item{
|
||||
publicId: rt.LastItemId,
|
||||
updateTime: rt.LastItemUpdatedTime,
|
||||
resourceType: rt.ResourceType,
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh refreshes a token's updated time. It accounts for overlapping
|
||||
// database transactions by subtracting UpdatedTimeBuffer from the
|
||||
// provided timestamp while ensuring that the updated time is never
|
||||
// before the created time of the token.
|
||||
func (rt *Token) Refresh(updatedTime time.Time) *Token {
|
||||
rt.UpdatedTime = updatedTime.Add(-UpdatedTimeBuffer)
|
||||
if rt.UpdatedTime.Before(rt.CreatedTime) {
|
||||
rt.UpdatedTime = rt.CreatedTime
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
// RefreshLastItem refreshes a token's updated time and last item.
|
||||
// It accounts for overlapping database transactions by subtracting
|
||||
// UpdatedTimeBuffer from the provided timestamp while ensuring that
|
||||
// the updated time is never before the created time of the token.
|
||||
func (rt *Token) RefreshLastItem(res boundary.Resource, updatedTime time.Time) *Token {
|
||||
rt.LastItemId = res.GetPublicId()
|
||||
rt.LastItemUpdatedTime = res.GetUpdateTime().AsTime()
|
||||
rt = rt.Refresh(updatedTime)
|
||||
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.InvalidParameter, op, "refresh token was missing its grants hash")
|
||||
}
|
||||
if !bytes.Equal(rt.GrantsHash, expectedGrantsHash) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "grants have changed since refresh token was issued")
|
||||
}
|
||||
if rt.CreatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidParameter, 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.InvalidParameter, op, "refresh token was expired")
|
||||
}
|
||||
if rt.UpdatedTime.Before(rt.CreatedTime) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token was updated before its creation time")
|
||||
}
|
||||
if rt.UpdatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token was updated in the future")
|
||||
}
|
||||
if rt.LastItemId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token missing last item ID")
|
||||
}
|
||||
if rt.LastItemUpdatedTime.After(time.Now()) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token last item was updated in the future")
|
||||
}
|
||||
if rt.ResourceType != expectedResourceType {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "refresh token resource type does not match expected resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,430 +0,0 @@
|
||||
// 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) {
|
||||
t.Parallel()
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
{
|
||||
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.InvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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) {
|
||||
t.Parallel()
|
||||
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 {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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)
|
||||
tok := &refreshtoken.Token{
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: createdTime,
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "tcp_1234567890",
|
||||
LastItemUpdatedTime: createdTime,
|
||||
}
|
||||
updatedTime := time.Now()
|
||||
newTok := tok.Refresh(updatedTime)
|
||||
|
||||
require.True(t, newTok.UpdatedTime.Equal(updatedTime.Add(-refreshtoken.UpdatedTimeBuffer)))
|
||||
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))
|
||||
}
|
||||
|
||||
func TestRefreshLastItem(t *testing.T) {
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
tok := &refreshtoken.Token{
|
||||
CreatedTime: fiveDaysAgo,
|
||||
UpdatedTime: fiveDaysAgo,
|
||||
ResourceType: resource.Target,
|
||||
GrantsHash: []byte("some hash"),
|
||||
LastItemId: "tcp_1234567890",
|
||||
LastItemUpdatedTime: fiveDaysAgo,
|
||||
}
|
||||
resUpdatedTime := fiveDaysAgo.AddDate(0, 0, 1)
|
||||
res := &fakeTargetResource{
|
||||
publicId: "tcp_0123456789",
|
||||
updateTime: timestamp.New(resUpdatedTime),
|
||||
}
|
||||
tokUpdatedTime := time.Now()
|
||||
newTok := tok.RefreshLastItem(res, tokUpdatedTime)
|
||||
|
||||
require.True(t, newTok.UpdatedTime.Equal(tokUpdatedTime.Add(-refreshtoken.UpdatedTimeBuffer)))
|
||||
require.True(t, newTok.CreatedTime.Equal(fiveDaysAgo))
|
||||
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(res.updateTime.AsTime()))
|
||||
}
|
||||
Loading…
Reference in new issue