568 lines
20 KiB
Go
568 lines
20 KiB
Go
// Copyright IBM Corp. 2020, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/api/authtokens"
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/util"
|
|
)
|
|
|
|
// AuthTokenIdSegmentCount are the number of segments, delineated by "_", that
|
|
// make up the auth token id inside an auth token.
|
|
// For example, an authtoken format should look something like at_1234567890_sometokenpayload
|
|
const AuthTokenIdSegmentCount = 2
|
|
|
|
// upsertUserAndAuthToken upserts a user and authToken using the data in the provided authtoken.
|
|
// If creating this user results in the number of users exceeding the limit of
|
|
// allowed users it deletes the oldest one.
|
|
func upsertUserAndAuthToken(ctx context.Context, reader db.Reader, writer db.Writer, bAddr string, at *authtokens.AuthToken) error {
|
|
const op = "cache.upsertUserAndAuthToken"
|
|
switch {
|
|
case util.IsNil(reader):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
|
|
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 part of an inflight transaction")
|
|
case util.IsNil(at):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "auth token is nil")
|
|
case bAddr == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold address is empty")
|
|
case at.Id == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty")
|
|
case at.UserId == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "auth token user id is empty")
|
|
}
|
|
{
|
|
// always make sure the user exists when adding a token
|
|
u := &user{
|
|
Id: at.UserId,
|
|
Address: bAddr,
|
|
}
|
|
onConflict := &db.OnConflict{
|
|
Target: db.Columns{"id"},
|
|
// Unset the deleted_at column if it was set to un-delete the user
|
|
Action: db.SetColumnValues(map[string]any{"deleted_at": "infinity"}),
|
|
}
|
|
if err := writer.Create(ctx, u, db.WithOnConflict(onConflict)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
{
|
|
st := &AuthToken{
|
|
Id: at.Id,
|
|
UserId: at.UserId,
|
|
LastAccessedTime: time.Now(),
|
|
ExpirationTime: at.ExpirationTime,
|
|
}
|
|
onConflict := &db.OnConflict{
|
|
Target: db.Columns{"id"},
|
|
Action: db.SetColumns([]string{"last_accessed_time", "expiration_time"}),
|
|
}
|
|
if err := writer.Create(ctx, st, db.WithOnConflict(onConflict)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var users []*user
|
|
// we only want users that have not been soft deleted
|
|
if err := reader.SearchWhere(ctx, &users, "true", []any{}, db.WithLimit(-1), db.WithTable(activeUserTableName)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if len(users) <= usersLimit {
|
|
return nil
|
|
}
|
|
|
|
var oldestUser *user
|
|
var oldestUsersTime *time.Time
|
|
for _, u := range users {
|
|
ats, err := listTokens(ctx, reader, u)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
for _, at := range ats {
|
|
if oldestUsersTime == nil || oldestUsersTime.After(at.LastAccessedTime) {
|
|
oldestUser = u
|
|
oldestUsersTime = &at.LastAccessedTime
|
|
}
|
|
}
|
|
}
|
|
if oldestUser != nil {
|
|
if _, err := deleteUser(ctx, writer, oldestUser); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddRawToken upserts the auth token's user and auth token in the db and
|
|
// stores the actual raw auth token in the repositories in memory storage.
|
|
// The raw token must be valid and present in boundary and be for a user that
|
|
// has permission to send a Read request for itself to boundary.
|
|
func (r *Repository) AddRawToken(ctx context.Context, bAddr string, rawToken string) error {
|
|
const op = "cache.(Repository).AddRawToken"
|
|
switch {
|
|
case rawToken == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold auth token is empty", errors.WithoutEvent())
|
|
case bAddr == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold address is empty", errors.WithoutEvent())
|
|
}
|
|
|
|
// rawToken should look something like at_1234567890_someencryptedpayload
|
|
atIdParts := strings.SplitN(rawToken, "_", 4)
|
|
if len(atIdParts) != 3 {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold auth token is is malformed", errors.WithoutEvent())
|
|
}
|
|
atId := strings.Join(atIdParts[:AuthTokenIdSegmentCount], "_")
|
|
|
|
var at *authtokens.AuthToken
|
|
{
|
|
var inMemAuthToken *authtokens.AuthToken
|
|
atV, inMem := r.idToKeyringlessAuthToken.Load(atId)
|
|
if inMem {
|
|
var ok bool
|
|
inMemAuthToken, ok = atV.(*authtokens.AuthToken)
|
|
if !ok {
|
|
return errors.New(ctx, errors.Internal, op, "unable to cast in memory auth token to *authtoken.AuthToken", errors.WithoutEvent())
|
|
}
|
|
}
|
|
t := &AuthToken{
|
|
Id: atId,
|
|
}
|
|
err := r.rw.LookupById(ctx, t)
|
|
switch {
|
|
case err != nil && !errors.IsNotFoundError(err):
|
|
return errors.Wrap(ctx, err, op)
|
|
case errors.IsNotFoundError(err) || !inMem:
|
|
// if we don't know about it in the cache or we don't know about
|
|
// this auth token in memory, get it from boundary to sure up the
|
|
// cache information about this auth token.
|
|
at, err = r.tokenReadFromBoundaryFn(ctx, bAddr, rawToken)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
case t.UserId != inMemAuthToken.UserId:
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user id doesn't match what is specified in the stored auth token", errors.WithoutEvent())
|
|
case inMem:
|
|
at = inMemAuthToken
|
|
}
|
|
}
|
|
|
|
if at != nil {
|
|
// The token is never returned from boundary except in the original auth
|
|
// request so we must set the token.
|
|
at.Token = rawToken
|
|
}
|
|
|
|
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
|
if err := upsertUserAndAuthToken(ctx, reader, writer, bAddr, at); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
r.idToKeyringlessAuthToken.Store(at.Id, at)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddKeyringToken adds a token to the repository. If the token's id in the
|
|
// keyring doesn't match the id provided an error is returned.
|
|
// The token must be valid and present in boundary and be for a user that
|
|
// has permission to send a self-Read request to boundary. The user id
|
|
// stored in the keyring must also match the user id returned from boundary.
|
|
func (r *Repository) AddKeyringToken(ctx context.Context, bAddr string, token KeyringToken) error {
|
|
const op = "cache.(Repository).AddKeyringToken"
|
|
switch {
|
|
case token.TokenName == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "token name is empty", errors.WithoutEvent())
|
|
case token.KeyringType == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "keyring type is empty", errors.WithoutEvent())
|
|
case token.AuthTokenId == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold auth token id is empty", errors.WithoutEvent())
|
|
case bAddr == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "threshold address is empty", errors.WithoutEvent())
|
|
}
|
|
kt := token.clone()
|
|
keyringStoredAt, err := r.tokenKeyringFn(kt.KeyringType, kt.TokenName)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup token in keyring, keyring type: %q, token name: %q", kt.KeyringType, kt.TokenName), errors.WithoutEvent())
|
|
}
|
|
if keyringStoredAt == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "unable to find token in the keyring specified", errors.WithoutEvent())
|
|
}
|
|
if kt.AuthTokenId != keyringStoredAt.Id {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "provided auth token id doesn't match the one stored", errors.WithoutEvent())
|
|
}
|
|
|
|
var at *authtokens.AuthToken
|
|
{
|
|
cachedAt := &AuthToken{
|
|
Id: kt.AuthTokenId,
|
|
}
|
|
err := r.rw.LookupById(ctx, cachedAt)
|
|
switch {
|
|
case err != nil && !errors.IsNotFoundError(err):
|
|
return errors.Wrap(ctx, err, op)
|
|
case errors.IsNotFoundError(err):
|
|
at, err = r.tokenReadFromBoundaryFn(ctx, bAddr, keyringStoredAt.Token)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
case cachedAt.UserId != keyringStoredAt.UserId:
|
|
return errors.New(ctx, errors.InvalidParameter, op, "user id doesn't match what is specified in the stored auth token", errors.WithoutEvent())
|
|
default:
|
|
at = keyringStoredAt
|
|
}
|
|
}
|
|
if _, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
|
if err := upsertUserAndAuthToken(ctx, reader, writer, bAddr, at); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
onConflict := &db.OnConflict{
|
|
Target: db.Columns{"keyring_type", "token_name"},
|
|
Action: db.SetColumns([]string{"auth_token_id"}),
|
|
}
|
|
if err := writer.Create(ctx, kt, db.WithOnConflict(onConflict)); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LookupToken returns the Token in the cache if one exists.
|
|
// Accepts withUpdateLastAccessedTime options. If withUpdateLastAccessedTime
|
|
// is provided, the last update time of the returned token will be updated to
|
|
// the current time and reflected in the db. The returned AuthToken will not
|
|
// have the updated time.
|
|
func (r *Repository) LookupToken(ctx context.Context, authTokenId string, opt ...Option) (*AuthToken, error) {
|
|
const op = "cache.(Repository).LookupToken"
|
|
switch {
|
|
case authTokenId == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty", errors.WithoutEvent())
|
|
}
|
|
|
|
at := &AuthToken{
|
|
Id: authTokenId,
|
|
}
|
|
if err := r.rw.LookupById(ctx, at); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
|
|
opts, err := getOpts(opt...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
|
|
if opts.withUpdateLastAccessedTime {
|
|
updatedT := &AuthToken{
|
|
Id: authTokenId,
|
|
LastAccessedTime: time.Now(),
|
|
}
|
|
if _, err := r.rw.Update(ctx, updatedT, []string{"LastAccessedTime"}, nil); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
}
|
|
return at, nil
|
|
}
|
|
|
|
// deleteKeyringToken deletes a keyring token
|
|
func (r *Repository) deleteKeyringToken(ctx context.Context, kt KeyringToken) error {
|
|
const op = "cache.(Repository).deleteKeyringToken"
|
|
switch {
|
|
case kt.KeyringType == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing keyring type")
|
|
case kt.TokenName == "":
|
|
return errors.New(ctx, errors.InvalidParameter, op, "token name type")
|
|
}
|
|
|
|
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
|
// TODO(https://github.com/go-gorm/gorm/issues/4879): Use the
|
|
// writer.Delete() function once the gorm bug is fixed. Until then
|
|
// the gorm driver for sqlite has an error which wont execute a
|
|
// delete correctly. as a work around we manually execute the
|
|
// query here.
|
|
n, err := writer.Exec(ctx, "delete from keyring_token where (keyring_type, token_name) = (?, ?)", []any{kt.KeyringType, kt.TokenName})
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
|
|
switch n {
|
|
case 1:
|
|
if err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, r.idToKeyringlessAuthToken); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
return nil
|
|
case 0:
|
|
return errors.New(ctx, errors.RecordNotFound, op, "token not found when attempting deletion", errors.WithoutEvent())
|
|
default:
|
|
return errors.New(ctx, errors.MultipleRecords, op, "multiple tokens deleted when one was requested", errors.WithoutEvent())
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cleanExpiredOrOrphanedAuthTokens removes all tokens which are older than the staleness limit
|
|
// or does not have either a keyring or keyringless reference to it.
|
|
func (r *Repository) cleanExpiredOrOrphanedAuthTokens(ctx context.Context) error {
|
|
const op = "cache.Repository.cleanExpiredOrOrphanedAuthTokens"
|
|
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
|
if err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, r.idToKeyringlessAuthToken); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
|
|
}
|
|
return nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
// cleanExpiredOrOrphanedAuthTokens removes all tokens which are older than the staleness limit
|
|
// or does not have either a keyring or keyringless reference to it.
|
|
func cleanExpiredOrOrphanedAuthTokens(ctx context.Context, writer db.Writer, idToKeyringlessAuthToken *sync.Map) error {
|
|
const op = "cache.cleanExpiredOrOrphanedAuthTokens"
|
|
switch {
|
|
case util.IsNil(writer):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
|
case idToKeyringlessAuthToken == nil:
|
|
return errors.New(ctx, errors.InvalidParameter, op, "keyringless auth token map is nil")
|
|
case !writer.IsTx(ctx):
|
|
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't part of an inflight transaction")
|
|
}
|
|
|
|
var keyringlessAuthTokens []string
|
|
idToKeyringlessAuthToken.Range(func(key, _ any) bool {
|
|
keyringlessAuthTokens = append(keyringlessAuthTokens, key.(string))
|
|
return true
|
|
})
|
|
|
|
deleteOrphanedAuthTokens := `
|
|
delete from auth_token
|
|
where
|
|
last_accessed_time < @last_accessed_time
|
|
or
|
|
-- sqlite stores expiration_time as a string in a format that might not match
|
|
-- what is being used by current_timestamp. Using datetime() makes the
|
|
-- formats match which allow the string comparison performed here to work
|
|
-- the same as a time comparison.
|
|
datetime(expiration_time) < current_timestamp
|
|
or
|
|
%s
|
|
`
|
|
args := []any{sql.Named("last_accessed_time", time.Now().Add(-tokenStalenessLimit))}
|
|
|
|
idInSection := "id not in (select auth_token_id from keyring_token)"
|
|
if len(keyringlessAuthTokens) > 0 {
|
|
// Note: We have to build the statement like this because if the slice of string
|
|
// is empty this gets converted to a query that says " and id not in (NULL)"
|
|
idInSection = fmt.Sprintf("(%s and id not in @keyringless_token_ids)", idInSection)
|
|
args = append(args, sql.Named("keyringless_token_ids", keyringlessAuthTokens))
|
|
}
|
|
|
|
if _, err := writer.Exec(ctx, fmt.Sprintf(deleteOrphanedAuthTokens, idInSection), args); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const activeUserTableName = "user_active" // users that have not been soft deleted
|
|
|
|
// lookupUser returns a user if one is present in the repository or nil if not.
|
|
func (r *Repository) lookupUser(ctx context.Context, id string) (*user, error) {
|
|
const op = "cache.(Repository).lookupUser"
|
|
switch {
|
|
case id == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty id")
|
|
}
|
|
ret := &user{Id: id}
|
|
// we only want users that have NOT been soft deleted
|
|
if err := r.rw.LookupById(ctx, ret, db.WithTable(activeUserTableName)); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// listUsers returns all known tokens in the cache
|
|
func (r *Repository) listUsers(ctx context.Context) ([]*user, error) {
|
|
const op = "cache.(Repository).listUsers"
|
|
var ret []*user
|
|
// we only want users that have NOT been soft deleted
|
|
if err := r.rw.SearchWhere(ctx, &ret, "true", nil, db.WithTable(activeUserTableName)); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// listTokens returns all known tokens in the cache for the provided user
|
|
func (r *Repository) listTokens(ctx context.Context, u *user) ([]*AuthToken, error) {
|
|
const op = "cache.(Repository).listTokens"
|
|
ret, err := listTokens(ctx, r.rw, u)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// listTokens returns all known tokens in the cache for the provided user using the provided reader
|
|
func listTokens(ctx context.Context, reader db.Reader, u *user) ([]*AuthToken, error) {
|
|
const op = "cache.listTokens"
|
|
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 missing")
|
|
}
|
|
var ret []*AuthToken
|
|
if err := reader.SearchWhere(ctx, &ret, "user_id = ?", []any{u.Id}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// listKeyringTokens returns all known keyring tokens in the cache for the provided auth token
|
|
func (r *Repository) listKeyringTokens(ctx context.Context, at *AuthToken) ([]*KeyringToken, error) {
|
|
const op = "cache.(Repository).listTokens"
|
|
switch {
|
|
case util.IsNil(at):
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token is nil")
|
|
case at.Id == "":
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty")
|
|
}
|
|
var ret []*KeyringToken
|
|
if err := r.rw.SearchWhere(ctx, &ret, "auth_token_id = ?", []any{at.Id}); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// syncKeyringlessTokensWithDb removes the in memory storage of auth tokens if
|
|
// they are no longer represented in the db.
|
|
func (r *Repository) syncKeyringlessTokensWithDb(ctx context.Context) error {
|
|
const op = "cache.(Repository).syncKeyringlessTokensWithDb"
|
|
var ret []*AuthToken
|
|
if err := r.rw.SearchWhere(ctx, &ret, "true", nil); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
authTokenIds := make(map[string]struct{})
|
|
for _, at := range ret {
|
|
authTokenIds[at.Id] = struct{}{}
|
|
}
|
|
r.idToKeyringlessAuthToken.Range(func(key, value any) bool {
|
|
k := key.(string)
|
|
if _, ok := authTokenIds[k]; !ok {
|
|
r.idToKeyringlessAuthToken.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// deleteUser executes a delete command using the provided db.Writer for the provided user.
|
|
func deleteUser(ctx context.Context, w db.Writer, u *user) (int, error) {
|
|
const op = "cache.deleteUser"
|
|
switch {
|
|
case util.IsNil(w):
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
|
case u.Id == "":
|
|
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing id")
|
|
}
|
|
const (
|
|
// delete the user if they don't have any refresh tokens which are
|
|
// newer than 20 days (the refresh token expiration time)
|
|
deleteStmt = "delete from user where id = ? and id not in (select user_id from refresh_token where DATETIME('now', '-20 days') < datetime(create_time) )"
|
|
|
|
// fallback to soft deleting the user
|
|
softDeleteStmt = "update user set deleted_at = (strftime('%Y-%m-%d %H:%M:%f','now')) where id = ?"
|
|
)
|
|
// see if we should delete the user
|
|
rowsAffected, err := w.Exec(ctx, deleteStmt, []any{u.Id})
|
|
switch {
|
|
case err != nil:
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
case rowsAffected > 0:
|
|
// if we deleted the user, we're done.
|
|
return rowsAffected, nil
|
|
}
|
|
|
|
// fallback to soft delete
|
|
rowsAffected, err = w.Exec(ctx, softDeleteStmt, []any{u.Id})
|
|
if err != nil {
|
|
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
return rowsAffected, nil
|
|
}
|
|
|
|
// user is a gorm model for the user table. It represents a user
|
|
type user struct {
|
|
Id string `gorm:"primaryKey"`
|
|
Address string `gorm:"default:null"`
|
|
}
|
|
|
|
func (*user) TableName() string {
|
|
return "user"
|
|
}
|
|
|
|
func (u *user) clone() *user {
|
|
return &user{
|
|
Id: u.Id,
|
|
Address: u.Address,
|
|
}
|
|
}
|
|
|
|
// AuthToken is a gorm model for the token table and is an auth token known by the
|
|
// boundary client side cache daemon.
|
|
type AuthToken struct {
|
|
Id string `gorm:"primaryKey"`
|
|
UserId string `gorm:"default:null"`
|
|
LastAccessedTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
|
ExpirationTime time.Time
|
|
}
|
|
|
|
func (*AuthToken) TableName() string {
|
|
return "auth_token"
|
|
}
|
|
|
|
// KeyringToken is a gorm model for the keyring stored token
|
|
type KeyringToken struct {
|
|
KeyringType string `gorm:"primaryKey"`
|
|
TokenName string `gorm:"primaryKey"`
|
|
AuthTokenId string `gorm:"default:null"`
|
|
}
|
|
|
|
func (*KeyringToken) TableName() string {
|
|
return "keyring_token"
|
|
}
|
|
|
|
func (kt *KeyringToken) clone() *KeyringToken {
|
|
return &KeyringToken{
|
|
KeyringType: kt.KeyringType,
|
|
TokenName: kt.TokenName,
|
|
AuthTokenId: kt.AuthTokenId,
|
|
}
|
|
}
|