Files
threshold/internal/refreshtoken/refresh_token.go
T
Johan Brandhorst-Satzkorn dcb67ca616 internal/refreshtoken: add refreshtoken package
The refreshtoken package encapsulates domain logic surrouding
the refresh token. A refresh token is used both for
paginating through a collection and for requesting any
updates to that collection.
2024-01-05 07:43:19 -08:00

157 lines
5.6 KiB
Go

// 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
}