diff --git a/internal/listtoken/item.go b/internal/listtoken/item.go new file mode 100644 index 0000000000..43a26d8b0d --- /dev/null +++ b/internal/listtoken/item.go @@ -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 +} diff --git a/internal/listtoken/item_test.go b/internal/listtoken/item_test.go new file mode 100644 index 0000000000..7abcb9d966 --- /dev/null +++ b/internal/listtoken/item_test.go @@ -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) + }) +} diff --git a/internal/listtoken/list_token.go b/internal/listtoken/list_token.go new file mode 100644 index 0000000000..a5da93a90a --- /dev/null +++ b/internal/listtoken/list_token.go @@ -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 +} diff --git a/internal/listtoken/list_token_test.go b/internal/listtoken/list_token_test.go new file mode 100644 index 0000000000..3d84bd7ac0 --- /dev/null +++ b/internal/listtoken/list_token_test.go @@ -0,0 +1,1239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package listtoken_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/listtoken" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_NewPaginationToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + createdTime time.Time + typ resource.Type + grantsHash []byte + lastItemId string + lastItemCreateTime time.Time + want *listtoken.Token + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid list+pagination token", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo, + want: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.PaginationToken{ + LastItemId: "some id", + LastItemCreateTime: fiveDaysAgo, + }, + }, + }, + { + name: "missing grants hash", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: nil, + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo, + wantErrString: "missing grants hash", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new created time", + createdTime: fiveDaysAgo.AddDate(1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo, + wantErrString: "create time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "old created time", + createdTime: fiveDaysAgo.AddDate(-1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo, + wantErrString: "create time is too old", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new updated time", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo.AddDate(1, 0, 0), + wantErrString: "last item create time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing last item id", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "", + lastItemCreateTime: fiveDaysAgo, + wantErrString: "missing last item ID", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new last item updated time", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + lastItemId: "some id", + lastItemCreateTime: fiveDaysAgo.AddDate(1, 0, 0), + wantErrString: "last item create 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 := listtoken.NewPagination(context.Background(), tt.createdTime, tt.typ, tt.grantsHash, tt.lastItemId, tt.lastItemCreateTime) + 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)) + }) + } +} + +func Test_NewStartRefreshToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + createdTime time.Time + typ resource.Type + grantsHash []byte + previousDeletedIdsTime time.Time + phaseLowerBound time.Time + want *listtoken.Token + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid list+start-refresh token", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + previousDeletedIdsTime: fiveDaysAgo, + phaseLowerBound: fiveDaysAgo, + want: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousPhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + }, + }, + }, + { + name: "missing grants hash", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: nil, + previousDeletedIdsTime: fiveDaysAgo, + phaseLowerBound: fiveDaysAgo, + wantErrString: "missing grants hash", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new created time", + createdTime: fiveDaysAgo.AddDate(1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + previousDeletedIdsTime: fiveDaysAgo, + phaseLowerBound: fiveDaysAgo, + wantErrString: "create time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "old created time", + createdTime: fiveDaysAgo.AddDate(-1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + previousDeletedIdsTime: fiveDaysAgo, + phaseLowerBound: fiveDaysAgo, + wantErrString: "create time is too old", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new previous deleted ids time", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + previousDeletedIdsTime: fiveDaysAgo.AddDate(1, 0, 0), + phaseLowerBound: fiveDaysAgo, + wantErrString: "previous deleted ids time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new previous phase upper bound", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + previousDeletedIdsTime: fiveDaysAgo, + phaseLowerBound: fiveDaysAgo.AddDate(1, 0, 0), + wantErrString: "previous phase upper bound 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 := listtoken.NewStartRefresh(context.Background(), tt.createdTime, tt.typ, tt.grantsHash, tt.previousDeletedIdsTime, tt.phaseLowerBound) + 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)) + }) + } +} + +func Test_NewRefreshToken(t *testing.T) { + t.Parallel() + timeNow := time.Now() + fiveDaysAgo := timeNow.AddDate(0, 0, -5) + fourDaysAgo := timeNow.AddDate(0, 0, -4) + tests := []struct { + name string + createdTime time.Time + typ resource.Type + grantsHash []byte + phaseUpperBound time.Time + phaseLowerBound time.Time + previousDeleteIdsTime time.Time + lastItemId string + lastItemUpdateTime time.Time + want *listtoken.Token + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid list+refresh token", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + want: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: timeNow, + PhaseLowerBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fourDaysAgo, + }, + }, + }, + { + name: "missing grants hash", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: nil, + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "missing grants hash", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new created time", + createdTime: fiveDaysAgo.AddDate(1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "create time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "old created time", + createdTime: fiveDaysAgo.AddDate(-1, 0, 0), + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "create time is too old", + wantErrCode: errors.InvalidParameter, + }, + { + name: "new previous deleted ids time", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo.AddDate(1, 0, 0), + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "previous deleted ids time is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase upper bound in future", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow.AddDate(1, 0, 0), + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "phase upper bound is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase lower bound in future", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo.AddDate(1, 0, 0), + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "phase lower bound is in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase lower bound newer than upper bound", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: fiveDaysAgo, + phaseLowerBound: timeNow, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "phase lower bound is after phase upper bound", + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing last item ID", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "", + lastItemUpdateTime: fourDaysAgo, + wantErrString: "missing last item ID", + wantErrCode: errors.InvalidParameter, + }, + { + name: "last item update time in future", + createdTime: fiveDaysAgo, + typ: resource.Target, + grantsHash: []byte("some hash"), + phaseUpperBound: timeNow, + phaseLowerBound: fiveDaysAgo, + previousDeleteIdsTime: fiveDaysAgo, + lastItemId: "some id", + lastItemUpdateTime: fourDaysAgo.AddDate(1, 0, 0), + wantErrString: "last item update 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 := listtoken.NewRefresh( + context.Background(), + tt.createdTime, + tt.typ, + tt.grantsHash, + tt.previousDeleteIdsTime, + tt.phaseUpperBound, + tt.phaseLowerBound, + tt.lastItemId, + tt.lastItemUpdateTime, + ) + 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)) + }) + } +} + +func Test_ValidateListToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + token *listtoken.Token + grantsHash []byte + resourceType resource.Type + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid token", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + }, + { + name: "nil token", + token: nil, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token was missing", + wantErrCode: errors.InvalidParameter, + }, + { + name: "no grants hash", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: nil, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token was missing its grants hash", + wantErrCode: errors.InvalidParameter, + }, + { + name: "changed grants hash", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + }, + grantsHash: []byte("some other hash"), + resourceType: resource.Target, + wantErrString: "grants have changed since list token was issued", + wantErrCode: errors.InvalidParameter, + }, + { + name: "created in the future", + token: &listtoken.Token{ + CreateTime: time.Now().AddDate(1, 0, 0), + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token was created in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "expired", + token: &listtoken.Token{ + CreateTime: time.Now().AddDate(0, 0, -31), + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token was expired", + wantErrCode: errors.InvalidParameter, + }, + { + name: "resource type mismatch", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + }, + grantsHash: []byte("some hash"), + resourceType: resource.SessionRecording, + wantErrString: "list token resource type does not match expected resource type", + 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 Test_ValidatePaginationToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + token *listtoken.Token + grantsHash []byte + resourceType resource.Type + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid token", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.PaginationToken{ + LastItemId: "s_1234567890", + LastItemCreateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + }, + { + name: "last item ID unset", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.PaginationToken{ + LastItemId: "", + LastItemCreateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list tokens's pagination component missing last item ID", + wantErrCode: errors.InvalidParameter, + }, + { + name: "updated in the future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.PaginationToken{ + LastItemId: "s_1234567890", + LastItemCreateTime: time.Now().AddDate(1, 0, 0), + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's pagination component's last item was created 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 Test_ValidateStartRefreshToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + token *listtoken.Token + grantsHash []byte + resourceType resource.Type + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid token", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousDeletedIdsTime: fiveDaysAgo, + PreviousPhaseUpperBound: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + }, + { + name: "previous phase upper bound before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousDeletedIdsTime: fiveDaysAgo, + PreviousPhaseUpperBound: fiveDaysAgo.AddDate(-1, 0, 0), + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's start refresh component's previous phase upper bound was before its creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "previous phase upper bound in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousDeletedIdsTime: fiveDaysAgo, + PreviousPhaseUpperBound: fiveDaysAgo.AddDate(1, 0, 0), + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's start refresh component's previous phase upper bound was in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "previous deleted ids time before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousDeletedIdsTime: fiveDaysAgo.AddDate(-1, 0, 0), + PreviousPhaseUpperBound: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's start refresh component previous deleted ids time was before its creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "previous deleted ids time in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousDeletedIdsTime: fiveDaysAgo.AddDate(1, 0, 0), + PreviousPhaseUpperBound: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's start refresh component previous deleted ids time was 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 Test_ValidateRefreshToken(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + tests := []struct { + name string + token *listtoken.Token + grantsHash []byte + resourceType resource.Type + wantErrString string + wantErrCode errors.Code + }{ + { + name: "valid token", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + }, + { + name: "phase upper bound before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo.AddDate(-1, 0, 0), + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's phase upper bound was before its creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase upper bound before phase lower bound", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo.AddDate(0, 0, 1), + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo.AddDate(0, 0, 2), + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's phase upper bound was before the phase lower bound", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase upper bound in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo.AddDate(1, 0, 0), + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's phase upper bound was in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase lower bound before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo.AddDate(-1, 0, 0), + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's phase lower bound was before its creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "phase lower bound in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo.AddDate(1, 0, 0), + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's phase lower bound was in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "previous deleted ids time before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo.AddDate(-1, 0, 0), + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component previous deleted ids time was before its creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "previous deleted ids time in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo.AddDate(1, 0, 0), + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component previous deleted ids time was in the future", + wantErrCode: errors.InvalidParameter, + }, + { + name: "emtpy last item id", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component missing last item ID", + wantErrCode: errors.InvalidParameter, + }, + { + name: "last item update before create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo.AddDate(-1, 0, 0), + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's last item was updated before the list token's creation time", + wantErrCode: errors.InvalidParameter, + }, + { + name: "last item update in future", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo.AddDate(1, 0, 0), + }, + }, + grantsHash: []byte("some hash"), + resourceType: resource.Target, + wantErrString: "list token's refresh component's 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 TestToken_LastItem(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + + tests := []struct { + name string + token *listtoken.Token + wantItemPublicId string + wantItemCreateTime *timestamp.Timestamp + wantItemUpdateTime *timestamp.Timestamp + wantResourceType resource.Type + wantErrString string + wantErrCode errors.Code + }{ + { + name: "refresh token returns item with update time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.RefreshToken{ + PhaseUpperBound: fiveDaysAgo.Add(time.Hour), + PreviousDeletedIdsTime: fiveDaysAgo, + PhaseLowerBound: fiveDaysAgo, + LastItemId: "some id", + LastItemUpdateTime: fiveDaysAgo, + }, + }, + wantItemPublicId: "some id", + wantItemCreateTime: nil, + wantItemUpdateTime: timestamp.New(fiveDaysAgo), + wantResourceType: resource.Target, + }, + { + name: "pagination token returns item with create time", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.PaginationToken{ + LastItemId: "some id", + LastItemCreateTime: fiveDaysAgo, + }, + }, + wantItemPublicId: "some id", + wantItemCreateTime: timestamp.New(fiveDaysAgo), + wantItemUpdateTime: nil, + wantResourceType: resource.Target, + }, + { + name: "start refresh token returns no item", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: &listtoken.StartRefreshToken{ + PreviousPhaseUpperBound: fiveDaysAgo, + PreviousDeletedIdsTime: fiveDaysAgo, + }, + }, + wantErrString: "start refresh tokens have no last item", + wantErrCode: errors.Internal, + }, + { + name: "nil subtype returns no item", + token: &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: nil, + }, + wantErrString: "unexpected token subtype", + wantErrCode: errors.Internal, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + item, err := tt.token.LastItem(context.Background()) + if tt.wantErrString != "" { + require.ErrorContains(t, err, tt.wantErrString) + require.Equal(t, errors.Convert(err).Code, tt.wantErrCode) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResourceType, item.GetResourceType()) + assert.Equal(t, tt.wantItemCreateTime, item.GetCreateTime()) + assert.Equal(t, tt.wantItemPublicId, item.GetPublicId()) + assert.Equal(t, tt.wantItemUpdateTime, item.GetUpdateTime()) + }) + } +} + +func TestToken_Transition(t *testing.T) { + t.Parallel() + fiveDaysAgo := time.Now().AddDate(0, 0, -5) + + t.Run("pagination token without complete listing transitions to pagination token", func(t *testing.T) { + t.Parallel() + createTime := fiveDaysAgo.Add(-time.Hour) + tk, err := listtoken.NewPagination( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + "some id", + createTime, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour) + err = tk.Transition(context.Background(), false, lastItem, deletedIdsTime, phaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.PaginationToken) + require.True(t, ok, "Subtype was %T, not PaginationToken", tk.Subtype) + assert.Equal(t, lastItem.createTime.AsTime(), st.LastItemCreateTime) + assert.Equal(t, lastItem.publicId, st.LastItemId) + }) + t.Run("pagination token with complete listing transitions to start refresh", func(t *testing.T) { + t.Parallel() + createTime := fiveDaysAgo.Add(-time.Hour) + tk, err := listtoken.NewPagination( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + "some id", + createTime, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour) + err = tk.Transition(context.Background(), true, lastItem, deletedIdsTime, phaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.StartRefreshToken) + require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype) + assert.Equal(t, fiveDaysAgo, st.PreviousDeletedIdsTime) + assert.Equal(t, fiveDaysAgo, st.PreviousPhaseUpperBound) + }) + t.Run("start refresh token without complete listing transitions to refresh token", func(t *testing.T) { + t.Parallel() + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour) + tk, err := listtoken.NewStartRefresh( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + deletedIdsTime, + phaseUpperBound, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + updateTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + newDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour) + newPhaseUpperBound := fiveDaysAgo.Add(5 * time.Hour) + err = tk.Transition(context.Background(), false, lastItem, newDeletedIdsTime, newPhaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.RefreshToken) + require.True(t, ok, "Subtype was %T, not RefreshToken", tk.Subtype) + assert.Equal(t, lastItem.updateTime.AsTime(), st.LastItemUpdateTime) + assert.Equal(t, lastItem.publicId, st.LastItemId) + assert.Equal(t, phaseUpperBound, st.PhaseLowerBound) + assert.Equal(t, newPhaseUpperBound, st.PhaseUpperBound) + assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime) + }) + t.Run("start refresh token with complete listing transitions to start refresh", func(t *testing.T) { + t.Parallel() + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour) + tk, err := listtoken.NewStartRefresh( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + deletedIdsTime, + phaseUpperBound, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + newDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour) + newPhaseUpperBound := fiveDaysAgo.Add(5 * time.Hour) + err = tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.StartRefreshToken) + require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype) + assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime) + assert.Equal(t, newPhaseUpperBound, st.PreviousPhaseUpperBound) + }) + t.Run("refresh token without complete listing transitions to refresh token", func(t *testing.T) { + t.Parallel() + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseLowerBound := fiveDaysAgo.Add(3 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(4 * time.Hour) + updateTime := fiveDaysAgo.Add(5 * time.Hour) + tk, err := listtoken.NewRefresh( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + deletedIdsTime, + phaseUpperBound, + phaseLowerBound, + "some id", + updateTime, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + updateTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour) + newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour) + err = tk.Transition(context.Background(), false, lastItem, newDeletedIdsTime, newPhaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.RefreshToken) + require.True(t, ok, "Subtype was %T, not RefreshToken", tk.Subtype) + assert.Equal(t, lastItem.updateTime.AsTime(), st.LastItemUpdateTime) + assert.Equal(t, lastItem.publicId, st.LastItemId) + assert.Equal(t, phaseLowerBound, st.PhaseLowerBound) + assert.Equal(t, phaseUpperBound, st.PhaseUpperBound) + assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime) + }) + t.Run("refresh token with complete listing transitions to start refresh", func(t *testing.T) { + t.Parallel() + deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour) + phaseLowerBound := fiveDaysAgo.Add(3 * time.Hour) + phaseUpperBound := fiveDaysAgo.Add(4 * time.Hour) + updateTime := fiveDaysAgo.Add(5 * time.Hour) + tk, err := listtoken.NewRefresh( + context.Background(), + fiveDaysAgo, + resource.Target, + []byte("some hash"), + deletedIdsTime, + phaseUpperBound, + phaseLowerBound, + "some id", + updateTime, + ) + require.NoError(t, err) + lastItem := &fakeTarget{ + publicId: "some other id", + createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour) + newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour) + err = tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound) + require.NoError(t, err) + st, ok := tk.Subtype.(*listtoken.StartRefreshToken) + require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype) + assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime) + assert.Equal(t, phaseUpperBound, st.PreviousPhaseUpperBound) + }) + t.Run("token without subtype errors", func(t *testing.T) { + t.Parallel() + tk := &listtoken.Token{ + CreateTime: fiveDaysAgo, + ResourceType: resource.Target, + GrantsHash: []byte("some hash"), + Subtype: nil, + } + lastItem := &fakeTarget{ + publicId: "some other id", + createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)), + } + newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour) + newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour) + err := tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound) + require.Error(t, err) + assert.ErrorContains(t, err, "unexpected token subtype") + }) +} + +type fakeTarget struct { + boundary.Resource + publicId string + updateTime *timestamp.Timestamp + createTime *timestamp.Timestamp +} + +func (m *fakeTarget) GetResourceType() resource.Type { + return resource.Target +} + +func (m *fakeTarget) GetPublicId() string { + return m.publicId +} + +func (m *fakeTarget) GetUpdateTime() *timestamp.Timestamp { + return m.updateTime +} + +func (m *fakeTarget) GetCreateTime() *timestamp.Timestamp { + return m.createTime +} diff --git a/internal/refreshtoken/item.go b/internal/refreshtoken/item.go deleted file mode 100644 index e675971c70..0000000000 --- a/internal/refreshtoken/item.go +++ /dev/null @@ -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 -} diff --git a/internal/refreshtoken/item_test.go b/internal/refreshtoken/item_test.go deleted file mode 100644 index 2811a3975f..0000000000 --- a/internal/refreshtoken/item_test.go +++ /dev/null @@ -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)) -} diff --git a/internal/refreshtoken/refresh_token.go b/internal/refreshtoken/refresh_token.go deleted file mode 100644 index 72444ee90b..0000000000 --- a/internal/refreshtoken/refresh_token.go +++ /dev/null @@ -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 -} diff --git a/internal/refreshtoken/refresh_token_test.go b/internal/refreshtoken/refresh_token_test.go deleted file mode 100644 index 300e31cb32..0000000000 --- a/internal/refreshtoken/refresh_token_test.go +++ /dev/null @@ -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())) -}