2020-07-07 07:44:49 -07:00
package authtoken
import (
"context"
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/authtoken/store"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-19 16:47:18 -04:00
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
2020-07-07 07:44:49 -07:00
)
var (
lastAccessedUpdateDuration = 10 * time . Minute
2020-09-10 16:37:45 -04:00
timeSkew = time . Duration ( 0 )
2020-07-07 07:44:49 -07:00
)
// A Repository stores and retrieves the persistent types in the authtoken
// package. It is not safe to use a repository concurrently.
type Repository struct {
2020-11-16 10:39:18 -08:00
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
timeToLiveDuration time . Duration
timeToStaleDuration time . Duration
2020-07-07 07:44:49 -07:00
}
// NewRepository creates a new Repository. The returned repository is not safe for concurrent go
// routines to access it.
2020-08-19 16:47:18 -04:00
func NewRepository ( r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * Repository , error ) {
2020-07-07 07:44:49 -07:00
switch {
case r == nil :
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "db.Reader: auth token: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
case w == nil :
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "db.Writer: auth token: %w" , errors . ErrInvalidParameter )
2020-08-19 16:47:18 -04:00
case kms == nil :
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "kms: auth token: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
2020-07-23 09:53:37 -07:00
opts := getOpts ( opt ... )
2020-11-16 10:39:18 -08:00
2020-07-07 07:44:49 -07:00
return & Repository {
2020-11-16 10:39:18 -08:00
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
timeToLiveDuration : opts . withTokenTimeToLiveDuration ,
timeToStaleDuration : opts . withTokenTimeToStaleDuration ,
2020-07-07 07:44:49 -07:00
}, nil
}
// CreateAuthToken inserts an Auth Token into the repository and returns a new Auth Token. The returned auth token
// contains the auth token value. The provided IAM User ID must be associated to the provided auth account id
// or an error will be returned. All options are ignored.
2020-08-19 16:47:18 -04:00
func ( r * Repository ) CreateAuthToken ( ctx context . Context , withIamUser * iam . User , withAuthAccountId string , opt ... Option ) ( * AuthToken , error ) {
if withIamUser == nil {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "create: auth token: no user: %w" , errors . ErrInvalidParameter )
2020-08-19 16:47:18 -04:00
}
if withIamUser . GetPublicId () == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "create: auth token: no user id: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
if withAuthAccountId == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "create: auth token: no auth account id: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
at := allocAuthToken ()
at . AuthAccountId = withAuthAccountId
id , err := newAuthTokenId ()
if err != nil {
return nil , fmt . Errorf ( "create: auth token id: %w" , err )
}
at . PublicId = id
token , err := newAuthToken ()
if err != nil {
return nil , fmt . Errorf ( "create: auth token value: %w" , err )
}
at . Token = token
2020-08-19 16:47:18 -04:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , withIamUser . GetScopeId (), kms . KeyPurposeDatabase )
if err != nil {
return nil , fmt . Errorf ( "create: unable to get database wrapper: %w" , err )
}
2020-07-28 05:59:09 -07:00
// We truncate the expiration time to the nearest second to make testing in different platforms with
2020-07-27 14:06:59 -07:00
// different time resolutions easier.
2020-11-16 10:39:18 -08:00
expiration , err := ptypes . TimestampProto ( time . Now (). Add ( r . timeToLiveDuration ). Truncate ( time . Second ))
2020-07-07 07:44:49 -07:00
if err != nil {
return nil , err
}
at . ExpirationTime = & timestamp . Timestamp { Timestamp : expiration }
2020-07-23 09:53:37 -07:00
var newAuthToken * writableAuthToken
2020-07-07 07:44:49 -07:00
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( read db . Reader , w db . Writer ) error {
2020-08-10 15:17:15 -04:00
acct := allocAuthAccount ()
acct . PublicId = withAuthAccountId
2020-07-07 07:44:49 -07:00
if err := read . LookupByPublicId ( ctx , acct ); err != nil {
return fmt . Errorf ( "create: auth token: auth account lookup: %w" , err )
}
2020-08-19 16:47:18 -04:00
if acct . GetIamUserId () != withIamUser . GetPublicId () {
return fmt . Errorf ( "create: auth token: auth account %q mismatch with iam user %q" , withAuthAccountId , withIamUser . GetPublicId ())
2020-07-07 07:44:49 -07:00
}
at . ScopeId = acct . GetScopeId ()
at . AuthMethodId = acct . GetAuthMethodId ()
at . IamUserId = acct . GetIamUserId ()
2020-07-23 09:53:37 -07:00
newAuthToken = at . toWritableAuthToken ()
2020-08-19 16:47:18 -04:00
if err := newAuthToken . encrypt ( ctx , databaseWrapper ); err != nil {
2020-07-07 07:44:49 -07:00
return err
}
2020-09-10 16:37:45 -04:00
// tokens are not replicated, so they don't need oplog entries.
if err := w . Create ( ctx , newAuthToken ); err != nil {
2020-07-07 07:44:49 -07:00
return err
}
newAuthToken . CtToken = nil
return nil
},
)
if err != nil {
return nil , fmt . Errorf ( "create: auth token: %v: %w" , at , err )
}
2020-07-23 09:53:37 -07:00
return newAuthToken . toAuthToken (), nil
2020-07-07 07:44:49 -07:00
}
// LookupAuthToken returns the AuthToken for the provided id. Returns nil, nil if no AuthToken is found for id.
// For security reasons, the actual token is not included in the returned AuthToken.
// All exported options are ignored.
func ( r * Repository ) LookupAuthToken ( ctx context . Context , id string , opt ... Option ) ( * AuthToken , error ) {
if id == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "lookup: auth token: missing public id: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
opts := getOpts ( opt ... )
at := allocAuthToken ()
at . PublicId = id
if err := r . reader . LookupByPublicId ( ctx , at ); err != nil {
2020-11-05 16:54:03 -05:00
if errors . Is ( err , errors . ErrRecordNotFound ) {
2020-07-07 07:44:49 -07:00
return nil , nil
}
return nil , fmt . Errorf ( "auth token: lookup: %w" , err )
}
if opts . withTokenValue {
2020-08-19 16:47:18 -04:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , at . GetScopeId (), kms . KeyPurposeDatabase , kms . WithKeyId ( at . GetKeyId ()))
if err != nil {
return nil , fmt . Errorf ( "lookup: unable to get database wrapper: %w" , err )
}
if err := at . decrypt ( ctx , databaseWrapper ); err != nil {
2020-07-07 07:44:49 -07:00
return nil , fmt . Errorf ( "lookup: auth token: cannot decrypt auth token value: %w" , err )
}
}
at . CtToken = nil
2020-08-19 16:47:18 -04:00
at . KeyId = ""
2020-07-07 07:44:49 -07:00
return at , nil
}
// ValidateToken returns a token from storage if the auth token with the provided id and token exists. The
// approximate last accessed time may be updated depending on how long it has been since the last time the token
// was validated. If a token is returned it is guaranteed to be valid. For security reasons, the actual token
// value is not included in the returned AuthToken. If no valid auth token is found nil, nil is returned.
// All options are ignored.
//
// NOTE: Do not log or add the token string to any errors to avoid leaking it as it is a secret.
func ( r * Repository ) ValidateToken ( ctx context . Context , id , token string , opt ... Option ) ( * AuthToken , error ) {
if token == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "validate token: auth token: missing token: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
if id == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "validate token: auth token: missing public id: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
retAT , err := r . LookupAuthToken ( ctx , id , withTokenValue ())
if err != nil {
retAT = nil
2020-11-05 16:54:03 -05:00
if errors . Is ( err , errors . ErrRecordNotFound ) {
2020-07-07 07:44:49 -07:00
return nil , nil
}
return nil , fmt . Errorf ( "validate token: %w" , err )
}
if retAT == nil {
return nil , nil
}
// If the token is too old or stale invalidate it and return nothing.
exp , err := ptypes . Timestamp ( retAT . GetExpirationTime (). GetTimestamp ())
if err != nil {
return nil , fmt . Errorf ( "validate token: expiration time : %w" , err )
}
lastAccessed , err := ptypes . Timestamp ( retAT . GetApproximateLastAccessTime (). GetTimestamp ())
if err != nil {
return nil , fmt . Errorf ( "validate token: last accessed time : %w" , err )
}
now := time . Now ()
2020-09-10 16:37:45 -04:00
sinceLastAccessed := now . Sub ( lastAccessed ) + timeSkew
// TODO (jimlambrt 9/2020) - investigate the need for the timeSkew and see
// if it can be eliminated.
2020-11-16 10:39:18 -08:00
if now . After ( exp . Add ( - timeSkew )) || sinceLastAccessed >= r . timeToStaleDuration {
2020-07-07 07:44:49 -07:00
// If the token has expired or has become too stale, delete it from the DB.
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
2020-07-23 09:53:37 -07:00
delAt := retAT . toWritableAuthToken ()
2020-09-10 16:37:45 -04:00
// tokens are not replicated, so they don't need oplog entries.
if _ , err := w . Delete ( ctx , delAt ); err != nil {
2020-07-07 07:44:49 -07:00
return fmt . Errorf ( "validate token: delete auth token: %w" , err )
}
retAT = nil
return nil
})
2020-09-10 16:37:45 -04:00
if err != nil {
return nil , err
}
2020-07-07 07:44:49 -07:00
return nil , nil
}
if retAT . GetToken () != token {
2020-07-08 09:09:09 -07:00
return nil , nil
2020-07-07 07:44:49 -07:00
}
// retAT.Token set to empty string so the value is not returned as described in the methods' doc.
retAT . Token = ""
if sinceLastAccessed >= lastAccessedUpdateDuration {
// To save the db from being updated too frequently, we only update the
// LastAccessTime if it hasn't been updated within lastAccessedUpdateDuration.
// TODO: Make this duration configurable.
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
2020-07-23 09:53:37 -07:00
at := retAT . toWritableAuthToken ()
2020-07-07 07:44:49 -07:00
// Setting the ApproximateLastAccessTime to null through using the null mask allows a defined db's
2020-09-10 16:37:45 -04:00
// trigger to set ApproximateLastAccessTime to the commit
// timestamp. Tokens are not replicated, so they don't need oplog entries.
2020-07-07 07:44:49 -07:00
rowsUpdated , err := w . Update (
ctx ,
at ,
nil ,
[] string { "ApproximateLastAccessTime" },
)
if err == nil && rowsUpdated > 1 {
2020-11-05 16:54:03 -05:00
return errors . ErrMultipleRecords
2020-07-07 07:44:49 -07:00
}
return err
},
)
}
if err != nil {
return nil , fmt . Errorf ( "validate token: auth token: %s: %w" , id , err )
}
return retAT , nil
}
2020-07-23 09:53:37 -07:00
// ListAuthTokens in an org and supports the WithLimit option.
func ( r * Repository ) ListAuthTokens ( ctx context . Context , withOrgId string , opt ... Option ) ([] * AuthToken , error ) {
if withOrgId == "" {
2020-11-05 16:54:03 -05:00
return nil , fmt . Errorf ( "list users: missing org id %w" , errors . ErrInvalidParameter )
2020-07-23 09:53:37 -07:00
}
opts := getOpts ( opt ... )
2020-11-16 10:39:18 -08:00
2020-07-23 09:53:37 -07:00
var authTokens [] * AuthToken
2020-11-16 10:39:18 -08:00
if err := r . reader . SearchWhere ( ctx , & authTokens , "auth_account_id in (select public_id from auth_account where scope_id = ?)" , [] interface {}{ withOrgId }, db . WithLimit ( opts . withLimit )); err != nil {
2020-07-23 09:53:37 -07:00
return nil , fmt . Errorf ( "list users: %w" , err )
}
for _ , at := range authTokens {
at . Token = ""
at . CtToken = nil
2020-08-19 16:47:18 -04:00
at . KeyId = ""
2020-07-23 09:53:37 -07:00
}
return authTokens , nil
}
2020-07-07 07:44:49 -07:00
// DeleteAuthToken deletes the token with the provided id from the repository returning a count of the
// number of records deleted. All options are ignored.
func ( r * Repository ) DeleteAuthToken ( ctx context . Context , id string , opt ... Option ) ( int , error ) {
if id == "" {
2020-11-05 16:54:03 -05:00
return db . NoRowsAffected , fmt . Errorf ( "delete: auth token: missing public id: %w" , errors . ErrInvalidParameter )
2020-07-07 07:44:49 -07:00
}
at , err := r . LookupAuthToken ( ctx , id )
if err != nil {
2020-11-05 16:54:03 -05:00
if errors . Is ( err , errors . ErrRecordNotFound ) {
2020-07-07 07:44:49 -07:00
return db . NoRowsAffected , nil
}
return db . NoRowsAffected , fmt . Errorf ( "delete: auth token: lookup %w" , err )
}
if at == nil {
return db . NoRowsAffected , nil
}
var rowsDeleted int
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
2020-07-23 09:53:37 -07:00
deleteAT := at . toWritableAuthToken ()
2020-09-10 16:37:45 -04:00
// tokens are not replicated, so they don't need oplog entries.
rowsDeleted , err = w . Delete ( ctx , deleteAT )
2020-07-07 07:44:49 -07:00
if err == nil && rowsDeleted > 1 {
2020-11-05 16:54:03 -05:00
return errors . ErrMultipleRecords
2020-07-07 07:44:49 -07:00
}
return err
},
)
if err != nil {
return db . NoRowsAffected , fmt . Errorf ( "delete: auth token: %s: %w" , id , err )
}
return rowsDeleted , nil
}
func allocAuthToken () * AuthToken {
fresh := & AuthToken {
AuthToken : & store . AuthToken {},
}
return fresh
}