2022-02-25 08:37:35 -05:00
// Copyright (c) HashiCorp, Inc.
2022-03-04 12:37:26 -05:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2020-09-16 12:02:02 -04:00
package session
import (
"context"
2021-09-30 08:47:34 -04:00
"database/sql"
2020-09-16 12:02:02 -04:00
"fmt"
2021-05-14 12:23:12 -05:00
"strings"
2021-07-14 09:14:24 -07:00
"time"
2020-09-16 12:02:02 -04:00
"github.com/hashicorp/boundary/internal/db"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2023-09-12 08:15:34 -04:00
"github.com/hashicorp/boundary/internal/event"
2022-10-06 16:52:26 +01:00
"github.com/hashicorp/boundary/internal/kms"
2022-02-25 08:37:35 -05:00
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
2020-09-16 12:02:02 -04:00
)
2022-02-25 08:37:35 -05:00
// ConnectionRepository is the session connection database repository.
type ConnectionRepository struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
// defaultLimit provides a default for limiting the number of results returned from the repo
defaultLimit int
2022-03-04 12:37:26 -05:00
// workerStateDelay is used by queries to account for a delay in state propagation between
// worker and controller
workerStateDelay time . Duration
2022-02-25 08:37:35 -05:00
}
// NewConnectionRepository creates a new session Connection Repository. Supports the options: WithLimit
// which sets a default limit on results returned by repo operations.
func NewConnectionRepository ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * ConnectionRepository , error ) {
const op = "sessionConnection.NewRepository"
if r == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil reader" )
}
if w == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil writer" )
}
if kms == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil kms" )
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
2022-03-07 16:52:11 -05:00
2022-02-25 08:37:35 -05:00
return & ConnectionRepository {
2022-05-05 10:29:30 -07:00
reader : r ,
writer : w ,
kms : kms ,
defaultLimit : opts . withLimit ,
workerStateDelay : opts . withWorkerStateDelay ,
2022-02-25 08:37:35 -05:00
}, nil
}
// list will return a listing of resources and honor the WithLimit option or the
// repo defaultLimit. Supports WithOrder option.
2022-10-25 18:29:38 -05:00
func ( r * ConnectionRepository ) list ( ctx context . Context , resources any , where string , args [] any , opt ... Option ) error {
2022-02-25 08:37:35 -05:00
const op = "session.(ConnectionRepository).list"
opts := getOpts ( opt ... )
limit := r . defaultLimit
var dbOpts [] db . Option
if opts . withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts . withLimit
}
dbOpts = append ( dbOpts , db . WithLimit ( limit ))
switch opts . withOrderByCreateTime {
case db . AscendingOrderBy :
dbOpts = append ( dbOpts , db . WithOrder ( "create_time asc" ))
case db . DescendingOrderBy :
dbOpts = append ( dbOpts , db . WithOrder ( "create_time" ))
}
if err := r . reader . SearchWhere ( ctx , resources , where , args , dbOpts ... ); err != nil {
return errors . Wrap ( ctx , err , op )
}
return nil
}
2022-10-06 16:52:26 +01:00
func ( r * ConnectionRepository ) updateBytesUpBytesDown ( ctx context . Context , conns ...* Connection ) error {
const op = "session.(ConnectionRepository).updateBytesUpBytesDown"
if len ( conns ) == 0 {
return nil
}
updateMask := [] string { "BytesUp" , "BytesDown" }
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
for _ , c := range conns {
_ , err := w . Update (
ctx ,
& Connection { PublicId : c . PublicId , BytesUp : c . BytesUp , BytesDown : c . BytesDown },
updateMask ,
nil ,
// The last update to these two fields should come from our
// connection closure logic, which does not use this
// function (see the closeConnections func). Therefore, here
// we shouldn't update bytes up and down if the connection
// has already been closed. This also guards against
// potential data races where a connection closure request
2025-01-09 10:26:37 -08:00
// and a worker statistics update happen close to each other in
2022-10-06 16:52:26 +01:00
// terms of timing.
db . WithWhere ( "closed_reason is null" ),
)
if err != nil {
// Returning an error will rollback the entire transaction.
// We don't want to bail out of an update batch if just one
// of the connections fails to update, but we still log it.
event . WriteError ( ctx , op , fmt . Errorf ( "failed to update bytes up and down for connection id %q: %w" , c . GetPublicId (), err ))
continue
}
}
return nil
})
return err
}
2022-02-25 08:37:35 -05:00
// AuthorizeConnection will check to see if a connection is allowed. Currently,
// that authorization checks:
// * the hasn't expired based on the session.Expiration
// * number of connections already created is less than session.ConnectionLimit
// If authorization is success, it creates/stores a new connection in the repo
// and returns it, along with its states. If the authorization fails, it
// an error with Code InvalidSessionState.
2024-07-23 14:19:21 -04:00
func ( r * ConnectionRepository ) AuthorizeConnection ( ctx context . Context , sessionId , workerId string ) ( * Connection , error ) {
2022-02-25 08:37:35 -05:00
const op = "session.(ConnectionRepository).AuthorizeConnection"
if sessionId == "" {
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , status . Error ( codes . FailedPrecondition , "missing session id" ), op , errors . WithCode ( errors . InvalidParameter ))
2022-02-25 08:37:35 -05:00
}
2023-06-06 17:09:08 -04:00
connectionId , err := newConnectionId ( ctx )
2022-02-25 08:37:35 -05:00
if err != nil {
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , err , op )
2022-02-25 08:37:35 -05:00
}
connection := AllocConnection ()
connection . PublicId = connectionId
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
2022-10-25 18:29:38 -05:00
rowsAffected , err := w . Exec ( ctx , authorizeConnectionCte , [] any {
2022-02-25 08:37:35 -05:00
sql . Named ( "session_id" , sessionId ),
sql . Named ( "public_id" , connectionId ),
sql . Named ( "worker_id" , workerId ),
})
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to authorize connection %s" , sessionId )))
}
if rowsAffected == 0 {
return errors . Wrap ( ctx , status . Errorf ( codes . PermissionDenied , "session %s is not authorized (not active, expired or connection limit reached)" , sessionId ), op , errors . WithCode ( errors . InvalidSessionState ))
}
if err := reader . LookupById ( ctx , & connection ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for session %s" , sessionId )))
}
return nil
},
)
if err != nil {
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , err , op )
2022-02-25 08:37:35 -05:00
}
2024-07-23 14:19:21 -04:00
return & connection , nil
2022-02-25 08:37:35 -05:00
}
2020-09-16 12:02:02 -04:00
// LookupConnection will look up a connection in the repository and return the connection
2024-07-23 14:19:21 -04:00
// with its state. If the connection is not found, it will return nil, nil.
2020-09-16 12:02:02 -04:00
// No options are currently supported.
2024-07-23 14:19:21 -04:00
func ( r * ConnectionRepository ) LookupConnection ( ctx context . Context , connectionId string , _ ... Option ) ( * Connection , error ) {
2022-02-25 08:37:35 -05:00
const op = "session.(ConnectionRepository).LookupConnection"
2020-09-16 12:02:02 -04:00
if connectionId == "" {
2024-07-23 14:19:21 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing connectionId id" )
2020-09-16 12:02:02 -04:00
}
connection := AllocConnection ()
connection . PublicId = connectionId
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( read db . Reader , w db . Writer ) error {
if err := read . LookupById ( ctx , & connection ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for %s" , connectionId )))
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 ) {
2024-07-23 14:19:21 -04:00
return nil , nil
2020-09-16 12:02:02 -04:00
}
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-16 12:02:02 -04:00
}
2024-07-23 14:19:21 -04:00
return & connection , nil
2020-09-16 12:02:02 -04:00
}
2021-05-14 14:57:15 -05:00
// ListConnectionsBySessionId will list connections by session ID. Supports the
// WithLimit and WithOrder options.
2022-02-25 08:37:35 -05:00
func ( r * ConnectionRepository ) ListConnectionsBySessionId ( ctx context . Context , sessionId string , opt ... Option ) ([] * Connection , error ) {
const op = "session.(ConnectionRepository).ListConnectionsBySessionId"
2021-05-14 14:57:15 -05:00
if sessionId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "no session ID supplied" )
2021-05-14 12:23:12 -05:00
}
2021-05-14 14:57:15 -05:00
var connections [] * Connection
2022-10-25 18:29:38 -05:00
err := r . list ( ctx , & connections , "session_id = ?" , [] any { sessionId }, opt ... ) // pass options, so WithLimit and WithOrder are supported
2020-09-16 12:02:02 -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 connections , nil
}
2022-03-09 10:46:25 -05:00
// ConnectConnection updates a connection in the repo with a state of "connected".
2024-07-23 14:19:21 -04:00
func ( r * ConnectionRepository ) ConnectConnection ( ctx context . Context , c ConnectWith ) ( * Connection , error ) {
2022-03-09 10:46:25 -05:00
const op = "session.(ConnectionRepository).ConnectConnection"
// ConnectWith.validate will check all the fields...
2023-06-06 17:09:08 -04:00
if err := c . validate ( ctx ); err != nil {
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , err , op )
2022-03-09 10:46:25 -05:00
}
var connection Connection
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
connection = AllocConnection ()
connection . PublicId = c . ConnectionId
connection . ClientTcpAddress = c . ClientTcpAddress
connection . ClientTcpPort = c . ClientTcpPort
connection . EndpointTcpAddress = c . EndpointTcpAddress
connection . EndpointTcpPort = c . EndpointTcpPort
connection . UserClientIp = c . UserClientIp
fieldMask := [] string {
"ClientTcpAddress" ,
"ClientTcpPort" ,
"EndpointTcpAddress" ,
"EndpointTcpPort" ,
"UserClientIp" ,
}
rowsUpdated , err := w . Update ( ctx , & connection , fieldMask , nil )
if err != nil {
return errors . Wrap ( ctx , err , op )
}
if err == nil && rowsUpdated > 1 {
// return err, which will result in a rollback of the update
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been updated" )
}
2024-07-23 14:19:21 -04:00
// Set the lower bound of the connected_time_range to indicate the connection is connected
rowsUpdated , err = w . Exec ( ctx , connectConnection , [] any {
sql . Named ( "public_id" , c . ConnectionId ),
})
2022-03-09 10:46:25 -05:00
if err != nil {
return errors . Wrap ( ctx , err , op )
}
2024-07-23 14:19:21 -04:00
if rowsUpdated != 1 {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to connect connection %s" , c . ConnectionId )))
2022-03-09 10:46:25 -05:00
}
2024-07-23 14:19:21 -04:00
if err := reader . LookupById ( ctx , & connection ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for connection %s" , c . ConnectionId )))
2022-03-09 10:46:25 -05:00
}
return nil
},
)
if err != nil {
2024-07-23 14:19:21 -04:00
return nil , errors . Wrap ( ctx , err , op )
2022-03-09 10:46:25 -05:00
}
2024-07-23 14:19:21 -04:00
return & connection , nil
2022-03-09 10:46:25 -05:00
}
2022-03-09 08:54:18 -05:00
// closeConnectionResp is just a wrapper for the response from CloseConnections.
2024-07-23 14:19:21 -04:00
// It wraps the connection and its state for each connection closed.
2022-03-09 08:54:18 -05:00
type closeConnectionResp struct {
2024-07-23 14:19:21 -04:00
Connection * Connection
ConnectionState ConnectionStatus
2022-03-09 08:54:18 -05:00
}
// closeConnections set's a connection's state to "closed" in the repo. It's
// called by a worker after it's closed a connection between the client and the
// endpoint
func ( r * ConnectionRepository ) closeConnections ( ctx context . Context , closeWith [] CloseWith , _ ... Option ) ([] closeConnectionResp , error ) {
const op = "session.(ConnectionRepository).closeConnections"
if len ( closeWith ) == 0 {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing connections" )
}
for _ , cw := range closeWith {
2023-06-06 17:09:08 -04:00
if err := cw . validate ( ctx ); err != nil {
2022-03-09 08:54:18 -05:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "%s was invalid" , cw . ConnectionId )))
}
}
var resp [] closeConnectionResp
_ , err := r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
for _ , cw := range closeWith {
updateConnection := AllocConnection ()
updateConnection . PublicId = cw . ConnectionId
updateConnection . BytesUp = cw . BytesUp
updateConnection . BytesDown = cw . BytesDown
updateConnection . ClosedReason = cw . ClosedReason . String ()
2024-07-23 14:19:21 -04:00
// updating the ClosedReason will trigger the session_connection to set the
// upper limit of connection_time_range to indicate the connection is closed.
2022-03-09 08:54:18 -05:00
rowsUpdated , err := w . Update (
ctx ,
& updateConnection ,
[] string { "BytesUp" , "BytesDown" , "ClosedReason" },
nil ,
)
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to update connection %s" , cw . ConnectionId )))
}
if rowsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "%d would have been updated for connection %s" , rowsUpdated , cw . ConnectionId ))
}
resp = append ( resp , closeConnectionResp {
2024-07-23 14:19:21 -04:00
Connection : & updateConnection ,
ConnectionState : StatusClosed ,
2022-03-09 08:54:18 -05:00
})
}
return nil
},
)
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
return resp , nil
}
2020-09-16 12:02:02 -04:00
// DeleteConnection will delete a connection from the repository.
2022-02-25 08:37:35 -05:00
func ( r * ConnectionRepository ) DeleteConnection ( ctx context . Context , publicId string , _ ... Option ) ( int , error ) {
const op = "session.(ConnectionRepository).DeleteConnection"
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
}
connection := AllocConnection ()
connection . PublicId = publicId
if err := r . reader . LookupByPublicId ( ctx , & connection ); 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 {
deleteConnection := connection . Clone ()
var err error
rowsDeleted , err = w . Delete (
ctx ,
deleteConnection ,
)
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
}
2024-01-19 23:21:57 +00:00
// closeOrphanedConnections looks for connections that are still active, but were not reported by the worker.
2022-03-07 15:37:24 -05:00
func ( r * ConnectionRepository ) closeOrphanedConnections ( ctx context . Context , workerId string , reportedConnections [] string ) ([] string , error ) {
const op = "session.(ConnectionRepository).closeOrphanedConnections"
2021-07-14 09:14:24 -07:00
2022-03-07 15:37:24 -05:00
var orphanedConns [] string
2021-07-14 09:14:24 -07:00
2022-10-25 18:29:38 -05:00
args := make ([] any , 0 , len ( reportedConnections ) + 2 )
2022-05-02 10:32:33 -04:00
args = append ( args , sql . Named ( "worker_id" , workerId ))
2022-03-07 15:37:24 -05:00
args = append ( args , sql . Named ( "worker_state_delay_seconds" , r . workerStateDelay . Seconds ()))
2021-07-14 09:14:24 -07:00
2022-03-07 15:37:24 -05:00
var notInClause string
if len ( reportedConnections ) > 0 {
notInClause = `and public_id not in (%s)`
params := make ([] string , len ( reportedConnections ))
for i , connId := range reportedConnections {
params [ i ] = fmt . Sprintf ( "@%d" , i )
args = append ( args , sql . Named ( fmt . Sprintf ( "%d" , i ), connId ))
}
notInClause = fmt . Sprintf ( notInClause , strings . Join ( params , "," ))
2021-07-14 09:14:24 -07:00
}
2024-10-01 10:16:29 -04:00
query := fmt . Sprintf ( closeOrphanedConnections , notInClause )
2022-03-07 15:37:24 -05:00
_ , err := r . writer . DoTx (
2021-07-14 09:14:24 -07:00
ctx ,
2022-03-07 15:37:24 -05:00
db . StdRetryCnt ,
db . ExpBackoff {},
2024-04-19 13:40:42 -04:00
func ( _ db . Reader , w db . Writer ) error {
2024-10-01 10:16:29 -04:00
rows , err := w . Query ( ctx , query , args )
2022-03-07 15:37:24 -05:00
if err != nil {
return errors . Wrap ( ctx , err , op )
}
defer rows . Close ()
for rows . Next () {
var connectionId string
if err := rows . Scan ( & connectionId ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
}
orphanedConns = append ( orphanedConns , connectionId )
}
2024-01-22 14:03:40 -05:00
if err := rows . Err (); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "error getting next row" ))
}
2022-03-07 15:37:24 -05:00
return nil
},
2021-07-14 09:14:24 -07:00
)
if err != nil {
2022-03-07 15:37:24 -05:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error comparing state" ))
2021-07-14 09:14:24 -07:00
}
2022-03-07 15:37:24 -05:00
return orphanedConns , nil
2021-07-14 09:14:24 -07:00
}