2020-09-16 12:02:02 -04:00
package session
import (
"context"
"crypto/ed25519"
2020-09-19 00:18:17 -04:00
"crypto/subtle"
2021-09-30 08:47:34 -04:00
"database/sql"
2020-09-16 12:02:02 -04:00
"fmt"
"strings"
2022-05-20 18:16:06 +00:00
"time"
2020-09-16 12:02:02 -04:00
2022-04-29 09:52:01 -04:00
"github.com/hashicorp/boundary/internal/boundary"
2020-09-16 12:02:02 -04:00
"github.com/hashicorp/boundary/internal/db"
2020-09-19 21:05:56 -04:00
"github.com/hashicorp/boundary/internal/db/timestamp"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-09-16 12:02:02 -04:00
"github.com/hashicorp/boundary/internal/kms"
2022-03-16 17:04:07 -04:00
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
2020-09-16 12:02:02 -04:00
)
// CreateSession inserts into the repository and returns the new Session with
// its State of "Pending". The following fields must be empty when creating a
2022-05-02 10:32:33 -04:00
// session: WorkerId, and PublicId. No options are
2020-09-16 12:02:02 -04:00
// currently supported.
2022-04-01 16:32:20 -04:00
func ( r * Repository ) CreateSession ( ctx context . Context , sessionWrapper wrapping . Wrapper , newSession * Session , workerAddresses [] string , _ ... Option ) ( * Session , ed25519 . PrivateKey , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).CreateSession"
2020-09-16 12:02:02 -04:00
if newSession == nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session" )
2020-09-16 12:02:02 -04:00
}
if newSession . PublicId != "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "public id is not empty" )
2020-09-16 12:02:02 -04:00
}
if len ( newSession . Certificate ) != 0 {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "certificate is not empty" )
2020-09-16 12:02:02 -04:00
}
if newSession . TargetId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing target id" )
2020-09-16 12:02:02 -04:00
}
if newSession . HostId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing host id" )
2020-09-16 12:02:02 -04:00
}
if newSession . UserId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
2020-09-16 12:02:02 -04:00
}
if newSession . HostSetId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing host set id" )
2020-09-16 12:02:02 -04:00
}
if newSession . AuthTokenId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing auth token id" )
2020-09-16 12:02:02 -04:00
}
if newSession . ScopeId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing scope id" )
2020-09-16 12:02:02 -04:00
}
if newSession . CtTofuToken != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "ct is not empty" )
2020-09-16 12:02:02 -04:00
}
if newSession . TofuToken != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "tofu token is not empty" )
2020-09-16 12:02:02 -04:00
}
2020-09-19 10:31:49 -04:00
if newSession . ExpirationTime == nil || newSession . ExpirationTime . Timestamp . AsTime (). IsZero () {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing expiration time" )
2020-09-19 10:31:49 -04:00
}
2022-04-01 16:32:20 -04:00
if len ( workerAddresses ) == 0 {
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing addresses" )
}
2020-09-16 12:02:02 -04:00
id , err := newId ()
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2022-04-01 16:32:20 -04:00
privKey , certBytes , err := newCert ( ctx , sessionWrapper , newSession . UserId , id , workerAddresses , newSession . ExpirationTime . Timestamp . AsTime ())
2020-09-16 12:02:02 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
newSession . Certificate = certBytes
newSession . PublicId = id
2022-03-16 17:04:07 -04:00
newSession . KeyId , err = sessionWrapper . KeyId ( ctx )
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to get session wrapper key id" ))
}
2020-09-16 12:02:02 -04:00
var returnedSession * Session
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( read db . Reader , w db . Writer ) error {
returnedSession = newSession . Clone ().( * Session )
2021-06-22 22:26:26 -04:00
returnedSession . DynamicCredentials = nil
2022-06-09 15:32:48 -07:00
returnedSession . StaticCredentials = nil
2020-09-16 12:02:02 -04:00
if err = w . Create ( ctx , returnedSession ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2021-09-16 08:53:11 -04:00
2021-06-22 22:26:26 -04:00
for _ , cred := range newSession . DynamicCredentials {
cred . SessionId = newSession . PublicId
2021-09-16 08:53:11 -04:00
}
2022-06-09 15:32:48 -07:00
var staticCreds [] interface {}
for _ , cred := range newSession . StaticCredentials {
cred . SessionId = newSession . PublicId
staticCreds = append ( staticCreds , cred )
}
if len ( staticCreds ) > 0 {
if err = w . CreateItems ( ctx , staticCreds ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to create static credentials" ))
}
// Get static creds back from the db for return
var c [] * StaticCredential
if err := read . SearchWhere ( ctx , & c , "session_id = ?" , [] interface {}{ newSession . PublicId }); err != nil {
return errors . Wrap ( ctx , err , op )
}
returnedSession . StaticCredentials = c
}
2021-09-16 08:53:11 -04:00
// TODO: after upgrading to gorm v2 this batch insert can be replaced, since gorm v2 supports batch inserts
q , batchInsertArgs , err := batchInsertsessionCredentialDynamic ( newSession . DynamicCredentials )
if err == nil {
rows , err := w . Query ( ctx , q , batchInsertArgs )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2021-09-16 08:53:11 -04:00
defer rows . Close ()
for rows . Next () {
var returnedCred DynamicCredential
2022-01-05 14:38:38 -05:00
w . ScanRows ( ctx , rows , & returnedCred )
2021-09-16 08:53:11 -04:00
returnedSession . DynamicCredentials = append ( returnedSession . DynamicCredentials , & returnedCred )
}
2021-06-22 22:26:26 -04:00
}
2021-09-16 08:53:11 -04:00
2020-09-16 12:02:02 -04:00
var foundStates [] * State
// trigger will create new "Pending" state
if foundStates , err = fetchStates ( ctx , read , returnedSession . PublicId ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
if len ( foundStates ) == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . SessionNotFound , op , fmt . Sprintf ( "no states found for new session %s" , returnedSession . PublicId ))
2020-09-20 15:54:39 -04:00
}
2022-01-14 14:31:08 -05:00
if len ( foundStates ) != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "%d states found for new session %s" , len ( foundStates ), returnedSession . PublicId ))
}
2020-09-20 15:54:39 -04:00
returnedSession . States = foundStates
if returnedSession . States [ 0 ]. Status != StatusPending {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidSessionState , op , fmt . Sprintf ( "new session %s state is not valid: %s" , returnedSession . PublicId , returnedSession . States [ 0 ]. Status ))
2020-09-16 12:02:02 -04:00
}
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2021-02-03 16:25:56 -08:00
return returnedSession , privKey , nil
2020-09-16 12:02:02 -04:00
}
// LookupSession will look up a session in the repository and return the session
// with its states. Returned States are ordered by start time descending. If the
// session is not found, it will return nil, nil, nil. No options are currently
// supported.
2022-02-25 08:37:35 -05:00
func ( r * Repository ) LookupSession ( ctx context . Context , sessionId string , _ ... Option ) ( * Session , * AuthzSummary , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).LookupSession"
2020-09-16 12:02:02 -04:00
if sessionId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session id" )
2020-09-16 12:02:02 -04:00
}
session := AllocSession ()
session . PublicId = sessionId
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( read db . Reader , w db . Writer ) error {
if err := read . LookupById ( ctx , & session ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for %s" , sessionId )))
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
states , err := fetchStates ( ctx , read , sessionId , db . WithOrder ( "start_time desc" ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
session . States = states
2021-06-22 22:26:26 -04:00
2022-06-09 15:32:48 -07:00
var dynamicCreds [] * DynamicCredential
if err := read . SearchWhere ( ctx , & dynamicCreds , "session_id = ?" , [] interface {}{ sessionId }); err != nil {
return errors . Wrap ( ctx , err , op )
}
if len ( dynamicCreds ) > 0 {
session . DynamicCredentials = dynamicCreds
}
var staticCreds [] * StaticCredential
if err := read . SearchWhere ( ctx , & staticCreds , "session_id = ?" , [] interface {}{ sessionId }); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2022-06-09 15:32:48 -07:00
if len ( staticCreds ) > 0 {
session . StaticCredentials = staticCreds
2021-06-22 22:26:26 -04:00
}
2021-11-15 15:26:08 -06:00
connections , err := fetchConnections ( ctx , read , sessionId , db . WithOrder ( "create_time desc" ))
if err != nil {
return errors . Wrap ( ctx , err , op )
}
session . Connections = connections
2020-09-16 12:02:02 -04:00
return nil
},
)
if err != nil {
2020-12-21 10:07:34 -08:00
if errors . IsNotFoundError ( err ) {
2020-09-20 21:18:07 -04:00
return nil , nil , nil
2020-09-16 12:02:02 -04:00
}
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
if len ( session . CtTofuToken ) > 0 {
2021-03-04 20:45:27 -05:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , session . ScopeId , kms . KeyPurposeDatabase )
2020-09-16 12:02:02 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2020-09-16 12:02:02 -04:00
}
if err := session . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "cannot decrypt session value" ))
2020-09-16 12:02:02 -04:00
}
} else {
session . CtTofuToken = nil
}
2020-09-20 15:54:39 -04:00
2020-09-20 21:18:07 -04:00
authzSummary , err := r . sessionAuthzSummary ( ctx , sessionId )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to get authz summary" ))
2020-09-20 21:18:07 -04:00
}
return & session , authzSummary , nil
2020-09-16 12:02:02 -04:00
}
2022-05-25 16:20:14 +00:00
// fetchAuthzProtectedSessionsByScope fetches sessions for the given scopes.
// Note that the sessions are not fully populated, and only contain the
// necessary information to implement the boundary.AuthzProtectedEntity
// interface. Supports the WithTerminated option.
func ( r * Repository ) fetchAuthzProtectedSessionsByScope (
ctx context . Context , scopeIds [] string , opt ... Option ,
) ( map [ string ][] boundary . AuthzProtectedEntity , error ) {
const op = "session.(Repository).fetchAuthzProtectedSessionsByScope"
2022-04-29 09:52:01 -04:00
2022-05-25 16:20:14 +00:00
opts := getOpts ( opt ... )
2022-04-29 09:52:01 -04:00
2022-05-25 16:20:14 +00:00
if len ( scopeIds ) == 0 {
2022-04-29 09:52:01 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "no scopes given" )
}
2022-05-25 16:20:14 +00:00
args := [] interface {}{
sql . Named ( "scope_ids" , "{" + strings . Join ( scopeIds , "," ) + "}" ),
}
var query string
if opts . withTerminated {
query = sessionPublicIdList
} else {
query = nonTerminatedSessionPublicIdList
}
2022-04-29 09:52:01 -04:00
rows , err := r . reader . Query ( ctx , query , args )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
defer rows . Close ()
sessionsMap := map [ string ][] boundary . AuthzProtectedEntity {}
for rows . Next () {
var ses Session
if err := r . reader . ScanRows ( ctx , rows , & ses ); err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
}
sessionsMap [ ses . GetScopeId ()] = append ( sessionsMap [ ses . GetScopeId ()], ses )
}
return sessionsMap , nil
}
2022-05-16 15:14:12 -07:00
// ListSessions lists sessions. Supports the WithLimit, WithScopeId, and WithSessionIds options.
2020-09-16 12:02:02 -04:00
func ( r * Repository ) ListSessions ( ctx context . Context , opt ... Option ) ([] * Session , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).ListSessions"
2020-09-16 12:02:02 -04:00
opts := getOpts ( opt ... )
var where [] string
var args [] interface {}
2020-09-20 15:54:39 -04:00
inClauseCnt := 0
2022-04-29 09:52:01 -04:00
switch len ( opts . withScopeIds ) {
case 0 :
case 1 :
inClauseCnt += 1
where , args = append ( where , fmt . Sprintf ( "scope_id = @%d" , inClauseCnt )), append ( args , sql . Named ( fmt . Sprintf ( "%d" , inClauseCnt ), opts . withScopeIds [ 0 ]))
default :
idsInClause := make ([] string , 0 , len ( opts . withScopeIds ))
for _ , id := range opts . withScopeIds {
2021-01-25 19:37:40 -05:00
inClauseCnt += 1
2022-04-29 09:52:01 -04:00
idsInClause , args = append ( idsInClause , fmt . Sprintf ( "@%d" , inClauseCnt )), append ( args , sql . Named ( fmt . Sprintf ( "%d" , inClauseCnt ), id ))
2021-01-25 19:37:40 -05:00
}
2022-04-29 09:52:01 -04:00
where = append ( where , fmt . Sprintf ( "scope_id in (%s)" , strings . Join ( idsInClause , "," )))
2021-02-02 13:37:32 -07:00
}
2022-04-29 09:52:01 -04:00
2021-02-02 13:37:32 -07:00
if opts . withUserId != "" {
2020-09-20 15:54:39 -04:00
inClauseCnt += 1
2021-09-30 08:47:34 -04:00
where , args = append ( where , fmt . Sprintf ( "user_id = @%d" , inClauseCnt )), append ( args , sql . Named ( fmt . Sprintf ( "%d" , inClauseCnt ), opts . withUserId ))
2020-09-20 15:54:39 -04:00
}
2022-04-29 09:52:01 -04:00
switch len ( opts . withSessionIds ) {
case 0 :
case 1 :
inClauseCnt += 1
where , args = append ( where , fmt . Sprintf ( "s.public_id = @%d" , inClauseCnt )), append ( args , sql . Named ( fmt . Sprintf ( "%d" , inClauseCnt ), opts . withSessionIds [ 0 ]))
default :
2020-09-22 10:41:42 -04:00
idsInClause := make ([] string , 0 , len ( opts . withSessionIds ))
for _ , id := range opts . withSessionIds {
inClauseCnt += 1
2021-09-30 08:47:34 -04:00
idsInClause , args = append ( idsInClause , fmt . Sprintf ( "@%d" , inClauseCnt )), append ( args , sql . Named ( fmt . Sprintf ( "%d" , inClauseCnt ), id ))
2020-09-22 10:41:42 -04:00
}
2021-01-25 19:37:40 -05:00
where = append ( where , fmt . Sprintf ( "s.public_id in (%s)" , strings . Join ( idsInClause , "," )))
2020-09-22 10:41:42 -04:00
}
2022-04-29 09:52:01 -04:00
2020-09-20 15:54:39 -04:00
var limit string
switch {
case opts . withLimit < 0 : // any negative number signals unlimited results
case opts . withLimit == 0 : // zero signals the default value and default limits
limit = fmt . Sprintf ( "limit %d" , r . defaultLimit )
default :
// non-zero signals an override of the default limit for the repo.
limit = fmt . Sprintf ( "limit %d" , opts . withLimit )
}
2021-04-02 19:13:48 -04:00
var withOrder string
switch opts . withOrderByCreateTime {
case db . AscendingOrderBy :
withOrder = "order by create_time asc"
case db . DescendingOrderBy :
2021-11-15 15:26:08 -06:00
fallthrough
default :
2021-04-02 19:13:48 -04:00
withOrder = "order by create_time"
2020-09-20 15:54:39 -04:00
}
var whereClause string
if len ( where ) > 0 {
2022-04-29 15:59:39 -04:00
whereClause = " where " + strings . Join ( where , " and " )
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
q := sessionList
2022-04-29 15:59:39 -04:00
query := fmt . Sprintf ( q , whereClause , withOrder , limit , withOrder )
2020-09-16 12:02:02 -04:00
2020-09-30 18:11:59 -04:00
rows , err := r . reader . Query ( ctx , query , args )
2020-09-20 15:54:39 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
defer rows . Close ()
2021-11-15 15:26:08 -06:00
var sessionsList [] * sessionListView
2020-09-20 15:54:39 -04:00
for rows . Next () {
2021-11-15 15:26:08 -06:00
var s sessionListView
2022-01-05 14:38:38 -05:00
if err := r . reader . ScanRows ( ctx , rows , & s ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
2020-09-20 15:54:39 -04:00
}
2021-11-15 15:26:08 -06:00
sessionsList = append ( sessionsList , & s )
2020-09-20 15:54:39 -04:00
}
2021-11-15 15:26:08 -06:00
sessions , err := r . convertToSessions ( ctx , sessionsList , withListingConvert ( true ))
2020-09-20 15:54:39 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
return sessions , nil
}
// DeleteSession will delete a session from the repository.
2021-02-03 16:25:56 -08:00
func ( r * Repository ) DeleteSession ( ctx context . Context , publicId string , _ ... Option ) ( int , error ) {
const op = "session.(Repository).DeleteSession"
2020-09-16 12:02:02 -04:00
if publicId == "" {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-09-16 12:02:02 -04:00
}
session := AllocSession ()
session . PublicId = publicId
if err := r . reader . LookupByPublicId ( ctx , & session ); err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for %s" , publicId )))
2020-09-16 12:02:02 -04:00
}
var rowsDeleted int
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
deleteSession := session . Clone ()
var err error
rowsDeleted , err = w . Delete (
ctx ,
deleteSession ,
)
2021-02-03 16:25:56 -08:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-02-03 16:25:56 -08:00
}
if rowsDeleted > 1 {
2020-09-16 12:02:02 -04:00
// return err, which will result in a rollback of the delete
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been deleted" )
2020-09-16 12:02:02 -04:00
}
2021-02-03 16:25:56 -08:00
return nil
2020-09-16 12:02:02 -04:00
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for %s" , publicId )))
2020-09-16 12:02:02 -04:00
}
return rowsDeleted , nil
}
2020-09-21 16:33:28 -04:00
// CancelSession sets a session's state to "canceling" in the repo. It's called
// when the user cancels a session and the controller wants to update the
// session state to "canceling" for the given reason, so the workers can get the
// "canceling signal" during their next status heartbeat. CancelSession is
2020-09-21 11:14:30 -04:00
// idempotent.
2020-09-20 15:54:39 -04:00
func ( r * Repository ) CancelSession ( ctx context . Context , sessionId string , sessionVersion uint32 ) ( * Session , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).CancelSession"
2020-09-18 18:12:41 -04:00
if sessionId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session id" )
2020-09-16 12:02:02 -04:00
}
2020-09-18 18:12:41 -04:00
if sessionVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session version" )
2020-09-16 12:02:02 -04:00
}
2020-09-21 16:33:28 -04:00
s , ss , err := r . updateState ( ctx , sessionId , sessionVersion , StatusCanceling )
2020-09-18 18:12:41 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2020-09-20 15:54:39 -04:00
s . States = ss
return s , nil
2020-09-18 18:12:41 -04:00
}
2020-09-16 12:02:02 -04:00
2020-10-01 10:13:57 -04:00
// TerminateCompletedSessions will terminate sessions in the repo based on:
// * sessions that have exhausted their connection limit and all their connections are closed.
// * sessions that are expired and all their connections are closed.
// * sessions that are canceling and all their connections are closed
// This function should called on a periodic basis a Controllers via it's
// "ticker" pattern.
func ( r * Repository ) TerminateCompletedSessions ( ctx context . Context ) ( int , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).TerminateCompletedSessions"
2020-10-01 10:13:57 -04:00
var rowsAffected int
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
var err error
rowsAffected , err = w . Exec ( ctx , termSessionsUpdate , nil )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-10-01 10:13:57 -04:00
}
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
2020-10-01 10:13:57 -04:00
}
return rowsAffected , nil
}
2022-03-09 08:54:18 -05:00
// terminateSessionIfPossible is called on connection close and will attempt to close the connection's
// session if the following conditions are met:
// * sessions that have exhausted their connection limit and all their connections are closed.
// * sessions that are expired and all their connections are closed.
// * sessions that are canceling and all their connections are closed
func ( r * Repository ) terminateSessionIfPossible ( ctx context . Context , sessionId string ) ( int , error ) {
const op = "session.(Repository).terminateSessionIfPossible"
rowsAffected := 0
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
var err error
rowsAffected , err = w . Exec ( ctx , terminateSessionIfPossible ,
[] interface {}{ sql . Named ( "public_id" , sessionId )})
if err != nil {
return errors . Wrap ( ctx , err , op )
}
return nil
},
)
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
return rowsAffected , nil
}
2022-02-25 08:37:35 -05:00
type AuthzSummary struct {
2020-09-19 21:05:56 -04:00
ExpirationTime * timestamp . Timestamp
2020-09-20 15:53:49 -04:00
ConnectionLimit int32
2020-09-19 21:05:56 -04:00
CurrentConnectionCount uint32
}
2022-02-25 08:37:35 -05:00
func ( r * Repository ) sessionAuthzSummary ( ctx context . Context , sessionId string ) ( * AuthzSummary , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).sessionAuthzSummary"
2021-09-30 08:47:34 -04:00
rows , err := r . reader . Query ( ctx , remainingConnectionsCte , [] interface {}{ sql . Named ( "session_id" , sessionId )})
2020-09-19 21:05:56 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-19 21:05:56 -04:00
}
defer rows . Close ()
2022-02-25 08:37:35 -05:00
var info * AuthzSummary
2020-09-19 21:05:56 -04:00
for rows . Next () {
if info != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . MultipleRecords , op , "query returned more than one row" )
2020-09-19 21:05:56 -04:00
}
2022-02-25 08:37:35 -05:00
info = & AuthzSummary {}
2022-01-05 14:38:38 -05:00
if err := r . reader . ScanRows ( ctx , rows , info ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
2020-09-19 21:05:56 -04:00
}
}
return info , nil
2020-09-19 14:21:09 -04:00
}
2020-09-16 12:02:02 -04:00
// ActivateSession will activate the session and is called by a worker after
2020-09-18 18:12:41 -04:00
// authenticating the session. The session must be in a "pending" state to be
// activated. States are ordered by start time descending. Returns an
2021-02-03 16:25:56 -08:00
// InvalidSessionState error code if a connection cannot be made because the session
2020-09-21 16:33:28 -04:00
// was canceled or terminated.
2022-05-16 15:14:12 -07:00
func ( r * Repository ) ActivateSession ( ctx context . Context , sessionId string , sessionVersion uint32 , tofuToken [] byte ) ( * Session , [] * State , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.(Repository).ActivateSession"
2020-09-16 12:02:02 -04:00
if sessionId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session id" )
2020-09-16 12:02:02 -04:00
}
if sessionVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
2020-09-18 18:12:41 -04:00
}
2020-09-19 00:18:17 -04:00
if len ( tofuToken ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing tofu token" )
2020-09-19 00:18:17 -04:00
}
2020-09-16 12:02:02 -04:00
updatedSession := AllocSession ()
updatedSession . PublicId = sessionId
var returnedStates [] * State
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
2021-09-30 08:47:34 -04:00
rowsAffected , err := w . Exec ( ctx , activateStateCte , [] interface {}{
sql . Named ( "session_id" , sessionId ),
sql . Named ( "version" , sessionVersion ),
})
2020-09-16 12:02:02 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to activate session %s" , sessionId )))
2020-09-16 12:02:02 -04:00
}
if rowsAffected == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidSessionState , op , "session is not in a pending state" )
2020-09-16 12:02:02 -04:00
}
foundSession := AllocSession ()
foundSession . PublicId = sessionId
2020-09-18 18:12:41 -04:00
if err := reader . LookupById ( ctx , & foundSession ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for %s" , sessionId )))
2020-09-16 12:02:02 -04:00
}
databaseWrapper , err := r . kms . GetWrapper ( ctx , foundSession . ScopeId , kms . KeyPurposeDatabase )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2020-09-16 12:02:02 -04:00
}
2020-09-19 00:18:17 -04:00
if len ( foundSession . TofuToken ) > 0 && subtle . ConstantTimeCompare ( foundSession . TofuToken , tofuToken ) != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . TokenMismatch , op , "tofu token mismatch" )
2020-09-19 00:18:17 -04:00
}
2020-09-16 12:02:02 -04:00
updatedSession . TofuToken = tofuToken
if err := updatedSession . encrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
rowsUpdated , err := w . Update ( ctx , & updatedSession , [] string { "CtTofuToken" }, nil )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
if err == nil && rowsUpdated > 1 {
// return err, which will result in a rollback of the update
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been updated" )
2020-09-16 12:02:02 -04:00
}
returnedStates , err = fetchStates ( ctx , reader , sessionId , db . WithOrder ( "start_time desc" ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
return & updatedSession , returnedStates , nil
}
2020-09-18 18:12:41 -04:00
// updateState will update the session's state using the session id and its
2020-09-21 11:14:30 -04:00
// version. updateState is idempotent. States are ordered by start time
// descending. No options are currently supported.
2021-02-03 16:25:56 -08:00
func ( r * Repository ) updateState ( ctx context . Context , sessionId string , sessionVersion uint32 , s Status , _ ... Option ) ( * Session , [] * State , error ) {
const op = "session.(Repository).updateState"
2020-09-16 12:02:02 -04:00
if sessionId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session id" )
2020-09-16 12:02:02 -04:00
}
if sessionVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
2020-09-16 12:02:02 -04:00
}
if s == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing session status" )
2020-09-16 12:02:02 -04:00
}
if s == StatusActive {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "you must call ActivateSession to update a session's state to active" )
2020-09-16 12:02:02 -04:00
}
2020-09-21 11:14:30 -04:00
var rowsAffected int
2020-09-16 12:02:02 -04:00
updatedSession := AllocSession ()
var returnedStates [] * State
2020-09-21 11:14:30 -04:00
_ , err := r . writer . DoTx (
2020-09-16 12:02:02 -04:00
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
2020-09-21 11:14:30 -04:00
var err error
2020-09-16 12:02:02 -04:00
// We need to update the session version as that's the aggregate
updatedSession . PublicId = sessionId
updatedSession . Version = uint32 ( sessionVersion ) + 1
rowsUpdated , err := w . Update ( ctx , & updatedSession , [] string { "Version" }, nil , db . WithVersion ( & sessionVersion ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
if rowsUpdated != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated session and %d rows updated" , rowsUpdated ))
2020-09-16 12:02:02 -04:00
}
2020-09-21 11:14:30 -04:00
if len ( updatedSession . CtTofuToken ) > 0 {
2021-03-04 20:45:27 -05:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , updatedSession . ScopeId , kms . KeyPurposeDatabase )
2020-09-21 11:14:30 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2020-09-21 11:14:30 -04:00
}
if err := updatedSession . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "cannot decrypt session value" ))
2020-09-21 11:14:30 -04:00
}
} else {
updatedSession . CtTofuToken = nil
2020-09-16 12:02:02 -04:00
}
2021-09-30 08:47:34 -04:00
rowsAffected , err = w . Exec ( ctx , updateSessionState , [] interface {}{
sql . Named ( "session_id" , sessionId ),
sql . Named ( "status" , s . String ()),
})
2020-09-21 11:14:30 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to update session %s state to %s" , sessionId , s . String ())))
2020-09-21 11:14:30 -04:00
}
if rowsAffected != 0 && rowsAffected != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated session %s to state %s and %d rows inserted (should be 0 or 1)" , sessionId , s . String (), rowsAffected ))
2020-09-21 11:14:30 -04:00
}
2020-09-16 12:02:02 -04:00
returnedStates , err = fetchStates ( ctx , reader , sessionId , db . WithOrder ( "start_time desc" ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2020-09-21 11:14:30 -04:00
if len ( returnedStates ) < 1 && returnedStates [ 0 ]. Status != s {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidSessionState , op , fmt . Sprintf ( "failed to update %s to a state of %s" , sessionId , s . String ()))
2020-09-21 11:14:30 -04:00
}
2020-09-16 12:02:02 -04:00
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error creating new state" ))
2020-09-16 12:02:02 -04:00
}
return & updatedSession , returnedStates , nil
}
2022-03-07 15:37:24 -05:00
// checkIfNoLongerActive checks the given sessions to see if they are in a
// non-active state, i.e. "canceling" or "terminated"
// It returns a []StateReport for each session that is not active, with its current status.
func ( r * Repository ) checkIfNoLongerActive ( ctx context . Context , reportedSessions [] string ) ([] StateReport , error ) {
const op = "session.(Repository).checkIfNotActive"
notActive := make ([] StateReport , 0 , len ( reportedSessions ))
args := make ([] interface {}, 0 , len ( reportedSessions ))
var inClause string
if len ( reportedSessions ) <= 0 {
return notActive , nil
}
inClause = `and session_id in (%s)`
params := make ([] string , len ( reportedSessions ))
for i , sessId := range reportedSessions {
params [ i ] = fmt . Sprintf ( "@%d" , i )
args = append ( args , sql . Named ( fmt . Sprintf ( "%d" , i ), sessId ))
}
inClause = fmt . Sprintf ( inClause , strings . Join ( params , "," ))
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
rows , err := r . reader . Query ( ctx , fmt . Sprintf ( checkIfNotActive , inClause ), args )
if err != nil {
return errors . Wrap ( ctx , err , op )
}
defer rows . Close ()
for rows . Next () {
var sessionId string
var status Status
if err := rows . Scan ( & sessionId , & status ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
}
notActive = append ( notActive , StateReport {
SessionId : sessionId ,
Status : status ,
})
}
return nil
},
)
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error checking if sessions are no longer active" ))
}
return notActive , nil
}
2022-05-20 18:16:06 +00:00
func ( r * Repository ) deleteSessionsTerminatedBefore ( ctx context . Context , threshold time . Duration ) ( int , error ) {
const op = "session.(Repository).deleteTerminated"
args := [] any {
sql . Named ( "threshold_seconds" , threshold . Seconds ()),
}
c , err := r . writer . Exec ( ctx , deleteTerminated , args )
if err != nil {
return 0 , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error deleting terminated sessions" ))
}
return c , nil
}
2020-09-16 12:02:02 -04:00
func fetchStates ( ctx context . Context , r db . Reader , sessionId string , opt ... db . Option ) ([] * State , error ) {
2021-02-03 16:25:56 -08:00
const op = "session.fetchStates"
2020-09-16 12:02:02 -04:00
var states [] * State
if err := r . SearchWhere ( ctx , & states , "session_id = ?" , [] interface {}{ sessionId }, opt ... ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
if len ( states ) == 0 {
return nil , nil
}
return states , nil
}
2021-11-15 15:26:08 -06:00
func fetchConnections ( ctx context . Context , r db . Reader , sessionId string , opt ... db . Option ) ([] * Connection , error ) {
const op = "session.fetchConnections"
var connections [] * Connection
if err := r . SearchWhere ( ctx , & connections , "session_id = ?" , [] interface {}{ sessionId }, opt ... ); err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
if len ( connections ) == 0 {
return nil , nil
}
return connections , nil
}