255 lines
8.1 KiB
Go
255 lines
8.1 KiB
Go
// Copyright IBM Corp. 2020, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
stderrors "errors"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/util"
|
|
)
|
|
|
|
// ErrRefreshNotSupported is returned whenever it is determined a boundary
|
|
// instance does not support refresh tokens.
|
|
var ErrRefreshNotSupported = stderrors.New("refresh tokens are not supported for this controller")
|
|
|
|
// RefreshTokenValue is the the type for the actual refresh token value handled
|
|
// by the client cache.
|
|
type RefreshTokenValue string
|
|
|
|
// sentinelNoRefreshToken is the value set in the refresh token table when
|
|
// Boundary returns resources but no refresh tokens. The presence of this value
|
|
// indicates that the boundary instance queried does not support refresh tokens.
|
|
const sentinelNoRefreshToken RefreshTokenValue = "__no_refresh_token_supported__"
|
|
|
|
// CacheSupport is an enum that identifies if a boundary instance can be supported
|
|
// by the client cache.
|
|
type CacheSupport string
|
|
|
|
const (
|
|
UnknownCacheSupport CacheSupport = "Unknown"
|
|
SupportedCacheSupport CacheSupport = "Supported"
|
|
NotSupportedCacheSupport CacheSupport = "Not Supported"
|
|
)
|
|
|
|
// cacheSupportCheckedState contains the state of a cache support check.
|
|
type cacheSupportCheckedState struct {
|
|
supported CacheSupport
|
|
lastChecked *time.Time
|
|
}
|
|
|
|
// cacheSupportState returns the cacheSupportCheckedState for the provided user
|
|
// which says if caching is supported for that user and when the last time it
|
|
// was checked.
|
|
func (r *Repository) cacheSupportState(ctx context.Context, u *user) (*cacheSupportCheckedState, error) {
|
|
const op = "cache.(Repository).cacheSupportState"
|
|
switch {
|
|
case util.IsNil(u):
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
}
|
|
var ret []*refreshToken
|
|
if err := r.rw.SearchWhere(ctx, &ret, "user_id = @user_id", []any{sql.Named("user_id", u.Id)}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if len(ret) == 0 {
|
|
return &cacheSupportCheckedState{supported: UnknownCacheSupport}, nil
|
|
}
|
|
|
|
lastChecked := time.Time{}
|
|
supported := UnknownCacheSupport
|
|
for _, rt := range ret {
|
|
if rt.UpdateTime.After(lastChecked) {
|
|
lastChecked = rt.UpdateTime
|
|
}
|
|
switch {
|
|
case supported == NotSupportedCacheSupport || rt.RefreshToken == sentinelNoRefreshToken:
|
|
supported = NotSupportedCacheSupport
|
|
default:
|
|
supported = SupportedCacheSupport
|
|
}
|
|
}
|
|
return &cacheSupportCheckedState{supported: supported, lastChecked: &lastChecked}, nil
|
|
}
|
|
|
|
// lookupRefreshToken returns the last known valid refresh token or an empty
|
|
// string if one is unknown. No error is returned if no valid refresh token is
|
|
// found.
|
|
func (r *Repository) lookupRefreshToken(ctx context.Context, u *user, resourceType resourceType) (*refreshToken, error) {
|
|
const op = "cache.(Repsoitory).lookupRefreshToken"
|
|
switch {
|
|
case util.IsNil(u):
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
case !resourceType.valid():
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
|
}
|
|
|
|
rt := &refreshToken{
|
|
UserId: u.Id,
|
|
ResourceType: resourceType,
|
|
}
|
|
if err := r.rw.LookupById(ctx, rt); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return rt, nil
|
|
}
|
|
|
|
// listRefreshTokens returns all refresh tokens associated with a specific user
|
|
func (r *Repository) listRefreshTokens(ctx context.Context, u *user) ([]*refreshToken, error) {
|
|
const op = "cache.(Repository).listRefreshTokens"
|
|
switch {
|
|
case util.IsNil(u):
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
}
|
|
var ret []*refreshToken
|
|
if err := r.rw.SearchWhere(ctx, &ret, "user_id = @user_id and refresh_token != @sentinel",
|
|
[]any{sql.Named("user_id", u.Id), sql.Named("sentinel", sentinelNoRefreshToken)}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// deleteRefreshToken deletes the refresh token for the provided user and resource type
|
|
func (r *Repository) deleteRefreshToken(ctx context.Context, u *user, rType resourceType) error {
|
|
const op = "cache.(Repository).deleteRefreshToken"
|
|
switch {
|
|
case util.IsNil(u):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
case !rType.valid():
|
|
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
|
}
|
|
|
|
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
|
|
return deleteRefreshToken(ctx, w, u, rType)
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deleteRefreshToken deletes the refresh token for the provided user and resource type
|
|
func deleteRefreshToken(ctx context.Context, w db.Writer, u *user, rType resourceType) error {
|
|
const op = "cache.deleteRefreshToken"
|
|
switch {
|
|
case util.IsNil(w):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
|
case !w.IsTx(ctx):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't in a transaction")
|
|
case util.IsNil(u):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
case !rType.valid():
|
|
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
|
}
|
|
rt := &refreshToken{
|
|
UserId: u.Id,
|
|
ResourceType: rType,
|
|
}
|
|
n, err := w.Delete(ctx, rt)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if n > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "attempted to delete a single resource but multiple were resource deletions were attempted")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upsertRefreshToken(ctx context.Context, writer db.Writer, u *user, rt resourceType, tok RefreshTokenValue) error {
|
|
const op = "cache.upsertRefreshToken"
|
|
switch {
|
|
case util.IsNil(writer):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
|
case !writer.IsTx(ctx):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't in a transaction")
|
|
case util.IsNil(u):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
|
case u.Id == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
|
case !rt.valid():
|
|
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
|
}
|
|
|
|
refTok := &refreshToken{
|
|
UserId: u.Id,
|
|
ResourceType: rt,
|
|
RefreshToken: tok,
|
|
UpdateTime: time.Now(),
|
|
CreateTime: time.Now(),
|
|
}
|
|
|
|
switch tok {
|
|
case "":
|
|
_, err := writer.Delete(ctx, refTok)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
default:
|
|
onConflict := &db.OnConflict{
|
|
Target: db.Columns{"user_id", "resource_type"},
|
|
Action: db.SetColumns([]string{"refresh_token", "update_time"}),
|
|
}
|
|
if err := writer.Create(ctx, refTok, db.WithOnConflict(onConflict)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type resourceType string
|
|
|
|
const (
|
|
unknownResourceType resourceType = "unknown"
|
|
targetResourceType resourceType = "target"
|
|
sessionResourceType resourceType = "session"
|
|
resolvableAliasResourceType resourceType = "resolvable-alias"
|
|
)
|
|
|
|
func (r resourceType) ColumnName() string {
|
|
switch r {
|
|
case resolvableAliasResourceType:
|
|
return "resolvable_alias"
|
|
default:
|
|
return string(r)
|
|
}
|
|
}
|
|
|
|
func (r resourceType) valid() bool {
|
|
switch r {
|
|
case resolvableAliasResourceType, targetResourceType, sessionResourceType:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
type refreshToken struct {
|
|
UserId string `gorm:"primaryKey"`
|
|
ResourceType resourceType `gorm:"primaryKey"`
|
|
RefreshToken RefreshTokenValue
|
|
UpdateTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
|
CreateTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
|
}
|
|
|
|
func (*refreshToken) TableName() string {
|
|
return "refresh_token"
|
|
}
|