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/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 ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.NewRepository"
2020-07-07 07:44:49 -07:00
switch {
case r == nil :
2021-07-29 18:52:56 -06:00
return nil , errors . NewDeprecated ( errors . InvalidParameter , op , "nil db reader" )
2020-07-07 07:44:49 -07:00
case w == nil :
2021-07-29 18:52:56 -06:00
return nil , errors . NewDeprecated ( errors . InvalidParameter , op , "nil db writer" )
2020-08-19 16:47:18 -04:00
case kms == nil :
2021-07-29 18:52:56 -06:00
return nil , errors . NewDeprecated ( errors . InvalidParameter , op , "nil kms" )
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
}
2021-04-08 20:54:32 -04:00
// 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. The Auth Token will have a Status of "issued".
// The WithStatus and WithPublicId options are supported and all other 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 ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.(Repository).CreateAuthToken"
2021-04-08 20:54:32 -04:00
if withIamUser == nil || withIamUser . User == nil {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user" )
2020-08-19 16:47:18 -04:00
}
if withIamUser . GetPublicId () == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
2020-07-07 07:44:49 -07:00
}
if withAuthAccountId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing auth account id" )
2020-07-07 07:44:49 -07:00
}
2021-02-10 13:52:42 -05:00
at , err := newAuthToken ()
2020-07-07 07:44:49 -07:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
2021-02-10 13:52:42 -05:00
at . AuthAccountId = withAuthAccountId
2021-04-08 20:54:32 -04:00
opts := getOpts ( opt ... )
if opts . withPublicId == "" {
id , err := NewAuthTokenId ()
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2021-04-08 20:54:32 -04:00
}
opts . withPublicId = id
}
at . PublicId = opts . withPublicId
switch {
case opts . withStatus != "" :
at . Status = string ( PendingStatus )
default :
at . Status = string ( IssuedStatus )
2020-07-07 07:44:49 -07:00
}
2020-08-19 16:47:18 -04:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , withIamUser . GetScopeId (), kms . KeyPurposeDatabase )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2020-08-19 16:47:18 -04:00
}
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 {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithCode ( errors . InvalidTimeStamp ))
2020-07-07 07:44:49 -07:00
}
at . ExpirationTime = & timestamp . Timestamp { Timestamp : expiration }
2021-02-10 13:52:42 -05:00
var newAuthToken * AuthToken
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 {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "auth account lookup" ))
2020-07-07 07:44:49 -07:00
}
2020-08-19 16:47:18 -04:00
if acct . GetIamUserId () != withIamUser . GetPublicId () {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op ,
2021-02-03 17:34:46 -06:00
fmt . Sprintf ( "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 ()
2021-02-10 13:52:42 -05:00
newAuthToken = at . clone ()
2020-08-19 16:47:18 -04:00
if err := newAuthToken . encrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
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 {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
newAuthToken . CtToken = nil
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
2021-02-10 13:52:42 -05:00
return newAuthToken , 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 ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.(Repository).LookupAuthToken"
2020-07-07 07:44:49 -07:00
if id == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidPublicId , op , "missing public id" )
2020-07-07 07:44:49 -07:00
}
opts := getOpts ( opt ... )
2021-02-10 13:52:42 -05:00
// use the view, to bring in the required account columns. Just don't forget
// to convert it before returning it.
atv := allocAuthTokenView ()
atv . PublicId = id
if err := r . reader . LookupByPublicId ( ctx , atv ); err != nil {
2020-12-21 10:07:34 -08:00
if errors . IsNotFoundError ( err ) {
2020-07-07 07:44:49 -07:00
return nil , nil
}
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
2021-02-10 13:52:42 -05:00
at := atv . toAuthToken ()
2020-07-07 07:44:49 -07:00
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 {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithCode ( errors . Encrypt ), errors . WithMsg ( "unable to get database wrapper" ))
2020-08-19 16:47:18 -04:00
}
if err := at . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
}
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 ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.(Repository).ValidateToken"
2020-07-07 07:44:49 -07:00
if token == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing token" )
2020-07-07 07:44:49 -07:00
}
if id == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidPublicId , op , "missing public id" )
2020-07-07 07:44:49 -07:00
}
retAT , err := r . LookupAuthToken ( ctx , id , withTokenValue ())
if err != nil {
retAT = nil
2020-12-21 10:07:34 -08:00
if errors . IsNotFoundError ( err ) {
2020-07-07 07:44:49 -07:00
return nil , nil
}
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
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 {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "expiration time" ), errors . WithCode ( errors . InvalidTimeStamp ))
2020-07-07 07:44:49 -07:00
}
lastAccessed , err := ptypes . Timestamp ( retAT . GetApproximateLastAccessTime (). GetTimestamp ())
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "last accessed time" ), errors . WithCode ( errors . InvalidTimeStamp ))
2020-07-07 07:44:49 -07:00
}
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 {
2021-02-10 13:52:42 -05:00
delAt := retAT . clone ()
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 {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "delete auth token" ))
2020-07-07 07:44:49 -07:00
}
retAT = nil
return nil
})
2020-09-10 16:37:45 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-10 16:37:45 -04:00
}
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 {
2021-02-10 13:52:42 -05:00
at := retAT . clone ()
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" },
)
2021-03-05 06:34:06 -08:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-03-05 06:34:06 -08:00
}
if rowsUpdated > 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been updated" )
2020-07-07 07:44:49 -07:00
}
2021-03-05 06:34:06 -08:00
return nil
2020-07-07 07:44:49 -07:00
},
)
}
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( id ))
2020-07-07 07:44:49 -07:00
}
return retAT , nil
}
2021-01-25 19:37:40 -05:00
// ListAuthTokens lists auth tokens in the given scopes and supports the
// WithLimit option.
func ( r * Repository ) ListAuthTokens ( ctx context . Context , withScopeIds [] string , opt ... Option ) ([] * AuthToken , error ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.(Repository).ListAuthTokens"
2021-01-25 19:37:40 -05:00
if len ( withScopeIds ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing scope id" )
2020-07-23 09:53:37 -07:00
}
opts := getOpts ( opt ... )
2020-11-16 10:39:18 -08:00
2021-02-10 13:52:42 -05:00
// use the view, to bring in the required account columns. Just don't forget
// to convert them before returning them
var atvs [] * authTokenView
if err := r . reader . SearchWhere ( ctx , & atvs , "auth_account_id in (select public_id from auth_account where scope_id in (?))" , [] interface {}{ withScopeIds }, db . WithLimit ( opts . withLimit )); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-07-23 09:53:37 -07:00
}
2021-02-10 13:52:42 -05:00
authTokens := make ([] * AuthToken , 0 , len ( atvs ))
for _ , atv := range atvs {
atv . Token = ""
atv . CtToken = nil
atv . KeyId = ""
authTokens = append ( authTokens , atv . toAuthToken ())
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 ) {
2021-02-03 17:34:46 -06:00
const op = "authtoken.(Repository).DeleteAuthToken"
2020-07-07 07:44:49 -07:00
if id == "" {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidPublicId , op , "missing public id" )
2020-07-07 07:44:49 -07:00
}
at , err := r . LookupAuthToken ( ctx , id )
if err != nil {
2020-12-21 10:07:34 -08:00
if errors . IsNotFoundError ( err ) {
2020-07-07 07:44:49 -07:00
return db . NoRowsAffected , nil
}
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
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 {
2021-02-10 13:52:42 -05:00
deleteAT := at . clone ()
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 )
2021-03-05 06:34:06 -08:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-07-07 07:44:49 -07:00
}
2021-03-05 06:34:06 -08:00
if rowsDeleted > 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been deleted" )
2021-03-05 06:34:06 -08:00
}
return nil
2020-07-07 07:44:49 -07:00
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( id ))
2020-07-07 07:44:49 -07:00
}
return rowsDeleted , nil
}
2021-04-08 20:54:32 -04:00
// IssueAuthToken will retrieve the "pending" token and update it's status to
// "issued". If the token has already been issued, an error is returned with a
// nil token. If no token is found for the tokenRequestId an error is returned
// with a nil token.
//
// Note: no oplog entries are created for auth token operations (this is intentional).
func ( r * Repository ) IssueAuthToken ( ctx context . Context , tokenRequestId string ) ( * AuthToken , error ) {
const op = "authtoken.(Repository).IssueAuthToken"
if tokenRequestId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing token request id" )
2021-04-08 20:54:32 -04:00
}
var at * AuthToken
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
at = allocAuthToken ()
at . PublicId = tokenRequestId
at . Status = string ( IssuedStatus )
// note: no oplog operations are created for auth token operations (this is intentional).
// Setting the ApproximateLastAccessTime to null through using the null mask allows a defined db's
// trigger to set ApproximateLastAccessTime to the commit timestamp.
rowsUpdated , err := w . Update ( ctx , at , [] string { "Status" }, [] string { "ApproximateLastAccessTime" }, db . WithWhere ( "status = ?" , PendingStatus ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-04-08 20:54:32 -04:00
}
if rowsUpdated == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . RecordNotFound , op , "pending auth token not found" )
2021-04-08 20:54:32 -04:00
}
if rowsUpdated > 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Internal , op , fmt . Sprintf ( "should have updated 1 row and we attempted to update %d rows" , rowsUpdated ))
2021-04-08 20:54:32 -04:00
}
if at . Status != string ( IssuedStatus ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Internal , op , "updated auth token status != issued" )
2021-04-08 20:54:32 -04:00
}
// we need a new repo, that's using the same reader/writer as this TxHandler
txRepo := Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
}
at , err = txRepo . LookupAuthToken ( ctx , at . PublicId , withTokenValue ())
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-04-08 20:54:32 -04:00
}
if at == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . RecordNotFound , op , "issued auth token not found" )
2021-04-08 20:54:32 -04:00
}
return nil
})
if err != nil {
return nil , err // error already wrapped when raised from r.DoTx(...)
}
return at , nil
}
// CloseExpiredPendingTokens will close expired pending tokens in the repo.
// This function should called on a periodic basis a Controllers via it's
// "ticker" pattern.
func ( r * Repository ) CloseExpiredPendingTokens ( ctx context . Context ) ( int , error ) {
const op = "authtoken.(Repository).CloseExpiredPendingTokens"
args := [] interface {}{ string ( FailedStatus ), string ( PendingStatus )}
const sql = `update auth_token set status = ? where status = ? and now() > expiration_time`
var tokensClosed int
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
var err error
tokensClosed , err = w . Exec ( ctx , sql , args )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-04-08 20:54:32 -04:00
}
return nil
},
)
if err != nil {
return db . NoRowsAffected , err // error already wrapped when raised from r.DoTx(...)
}
return tokensClosed , nil
}