mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
400 lines
16 KiB
400 lines
16 KiB
package session
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"math/big"
|
|
mathrand "math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db/timestamp"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping"
|
|
"github.com/hashicorp/go-kms-wrapping/structwrapping"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
const (
|
|
defaultSessionTableName = "session"
|
|
)
|
|
|
|
// ComposedOf defines the boundary data that is referenced to compose a session.
|
|
type ComposedOf struct {
|
|
// UserId of the session
|
|
UserId string
|
|
// HostId of the session
|
|
HostId string
|
|
// TargetId of the session
|
|
TargetId string
|
|
// HostSetId of the session
|
|
HostSetId string
|
|
// AuthTokenId of the session
|
|
AuthTokenId string
|
|
// ScopeId of the session
|
|
ScopeId string
|
|
// Endpoint. This is generated by the target, but is not stored in the
|
|
// warehouse as the worker may need to e.g. resolve DNS. This is to round
|
|
// trip the information to the worker when it validates a session.
|
|
Endpoint string
|
|
// Expiration time for the session
|
|
ExpirationTime *timestamp.Timestamp
|
|
// Max connections for the session
|
|
ConnectionLimit int32
|
|
}
|
|
|
|
// Session contains information about a user's session with a target
|
|
type Session struct {
|
|
// PublicId is used to access the session via an API
|
|
PublicId string `json:"public_id,omitempty" gorm:"primary_key"`
|
|
// UserId for the session
|
|
UserId string `json:"user_id,omitempty" gorm:"default:null"`
|
|
// HostId of the session
|
|
HostId string `json:"host_id,omitempty" gorm:"default:null"`
|
|
// ServerId that proxied the session
|
|
ServerId string `json:"server_id,omitempty" gorm:"default:null"`
|
|
// ServerType that proxied the session
|
|
ServerType string `json:"server_type,omitempty" gorm:"default:null"`
|
|
// TargetId for the session
|
|
TargetId string `json:"target_id,omitempty" gorm:"default:null"`
|
|
// HostSetId for the session
|
|
HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"`
|
|
// AuthTokenId for the session
|
|
AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"`
|
|
// ScopeId for the session
|
|
ScopeId string `json:"scope_id,omitempty" gorm:"default:null"`
|
|
// Certificate to use when connecting (or if using custom certs, to
|
|
// serve as the "login"). Raw DER bytes. Private key is not, and should not be
|
|
// stored in the database.
|
|
Certificate []byte `json:"certificate,omitempty" gorm:"default:null"`
|
|
// ExpirationTime - after this time the connection will be expired, e.g. forcefully terminated
|
|
ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"`
|
|
// CtTofuToken is the ciphertext Tofutoken value stored in the database
|
|
CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"`
|
|
// TofuToken - plain text of the "trust on first use" token for session
|
|
TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"`
|
|
// termination_reason for the session
|
|
TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"`
|
|
// CreateTime from the RDBMS
|
|
CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
|
// UpdateTime from the RDBMS
|
|
UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
|
// Version for the session
|
|
Version uint32 `json:"version,omitempty" gorm:"default:null"`
|
|
// Endpoint
|
|
Endpoint string `json:"-" gorm:"default:null"`
|
|
// Maximum number of connections in a session
|
|
ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"`
|
|
|
|
// key_id is the key ID that was used for the encryption operation. It can be
|
|
// used to identify a specific version of the key needed to decrypt the value,
|
|
// which is useful for caching purposes.
|
|
// @inject_tag: `gorm:"not_null"`
|
|
KeyId string `json:"key_id,omitempty" gorm:"not_null"`
|
|
|
|
// States for the session which are for read only and are ignored during
|
|
// write operations
|
|
States []*State `gorm:"-"`
|
|
tableName string `gorm:"-"`
|
|
}
|
|
|
|
func (s *Session) GetPublicId() string {
|
|
return s.PublicId
|
|
}
|
|
|
|
var _ Cloneable = (*Session)(nil)
|
|
var _ db.VetForWriter = (*Session)(nil)
|
|
|
|
// New creates a new in memory session.
|
|
func New(c ComposedOf, opt ...Option) (*Session, error) {
|
|
s := Session{
|
|
UserId: c.UserId,
|
|
HostId: c.HostId,
|
|
TargetId: c.TargetId,
|
|
HostSetId: c.HostSetId,
|
|
AuthTokenId: c.AuthTokenId,
|
|
ScopeId: c.ScopeId,
|
|
Endpoint: c.Endpoint,
|
|
ExpirationTime: c.ExpirationTime,
|
|
ConnectionLimit: c.ConnectionLimit,
|
|
}
|
|
if err := s.validateNewSession("new session:"); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// AllocSession will allocate a Session
|
|
func AllocSession() Session {
|
|
return Session{}
|
|
}
|
|
|
|
// Clone creates a clone of the Session
|
|
func (s *Session) Clone() interface{} {
|
|
clone := &Session{
|
|
PublicId: s.PublicId,
|
|
UserId: s.UserId,
|
|
HostId: s.HostId,
|
|
ServerId: s.ServerId,
|
|
ServerType: s.ServerType,
|
|
TargetId: s.TargetId,
|
|
HostSetId: s.HostSetId,
|
|
AuthTokenId: s.AuthTokenId,
|
|
ScopeId: s.ScopeId,
|
|
TerminationReason: s.TerminationReason,
|
|
Version: s.Version,
|
|
Endpoint: s.Endpoint,
|
|
ConnectionLimit: s.ConnectionLimit,
|
|
}
|
|
if len(s.States) > 0 {
|
|
clone.States = make([]*State, 0, len(s.States))
|
|
for _, ss := range s.States {
|
|
cp := ss.Clone().(*State)
|
|
clone.States = append(clone.States, cp)
|
|
}
|
|
}
|
|
if s.TofuToken != nil {
|
|
clone.TofuToken = make([]byte, len(s.TofuToken))
|
|
copy(clone.TofuToken, s.TofuToken)
|
|
}
|
|
if s.CtTofuToken != nil {
|
|
clone.CtTofuToken = make([]byte, len(s.CtTofuToken))
|
|
copy(clone.CtTofuToken, s.CtTofuToken)
|
|
}
|
|
if s.Certificate != nil {
|
|
clone.Certificate = make([]byte, len(s.Certificate))
|
|
copy(clone.Certificate, s.Certificate)
|
|
}
|
|
if s.ExpirationTime != nil {
|
|
clone.ExpirationTime = ×tamp.Timestamp{
|
|
Timestamp: ×tamppb.Timestamp{
|
|
Seconds: s.ExpirationTime.Timestamp.Seconds,
|
|
Nanos: s.ExpirationTime.Timestamp.Nanos,
|
|
},
|
|
}
|
|
}
|
|
if s.CreateTime != nil {
|
|
clone.CreateTime = ×tamp.Timestamp{
|
|
Timestamp: ×tamppb.Timestamp{
|
|
Seconds: s.CreateTime.Timestamp.Seconds,
|
|
Nanos: s.CreateTime.Timestamp.Nanos,
|
|
},
|
|
}
|
|
}
|
|
if s.UpdateTime != nil {
|
|
clone.UpdateTime = ×tamp.Timestamp{
|
|
Timestamp: ×tamppb.Timestamp{
|
|
Seconds: s.UpdateTime.Timestamp.Seconds,
|
|
Nanos: s.UpdateTime.Timestamp.Nanos,
|
|
},
|
|
}
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// VetForWrite implements db.VetForWrite() interface and validates the session
|
|
// before it's written.
|
|
func (s *Session) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
|
|
opts := db.GetOpts(opt...)
|
|
if s.PublicId == "" {
|
|
return fmt.Errorf("session vet for write: missing public id: %w", db.ErrInvalidParameter)
|
|
}
|
|
switch opType {
|
|
case db.CreateOp:
|
|
if err := s.validateNewSession("session vet for write:"); err != nil {
|
|
return err
|
|
}
|
|
if len(s.Certificate) == 0 {
|
|
return fmt.Errorf("session vet for write: certificate is missing: %w", db.ErrInvalidParameter)
|
|
}
|
|
case db.UpdateOp:
|
|
switch {
|
|
case contains(opts.WithFieldMaskPaths, "PublicId"):
|
|
return fmt.Errorf("session vet for write: public id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "UserId"):
|
|
return fmt.Errorf("session vet for write: user id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "HostId"):
|
|
return fmt.Errorf("session vet for write: host id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "TargetId"):
|
|
return fmt.Errorf("session vet for write: target id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "HostSetId"):
|
|
return fmt.Errorf("session vet for write: host set id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "AuthTokenId"):
|
|
return fmt.Errorf("session vet for write: auth token id is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "Certificate"):
|
|
return fmt.Errorf("session vet for write: certificate is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "CreateTime"):
|
|
return fmt.Errorf("session vet for write: create time is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "UpdateTime"):
|
|
return fmt.Errorf("session vet for write: update time is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "Endpoint"):
|
|
return fmt.Errorf("session vet for write: endpoint is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "ExpirationTime"):
|
|
return fmt.Errorf("session vet for write: expiration time is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "ConnectionLimit"):
|
|
return fmt.Errorf("session vet for write: connection limit is immutable: %w", db.ErrInvalidParameter)
|
|
case contains(opts.WithFieldMaskPaths, "TerminationReason"):
|
|
if _, err := convertToReason(s.TerminationReason); err != nil {
|
|
return fmt.Errorf("session vet for write: termination reason '%s' is invalid: %w", s.TerminationReason, db.ErrInvalidParameter)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TableName returns the tablename to override the default gorm table name
|
|
func (s *Session) TableName() string {
|
|
if s.tableName != "" {
|
|
return s.tableName
|
|
}
|
|
return defaultSessionTableName
|
|
}
|
|
|
|
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
|
// interface. If the caller attempts to set the name to "" the name will be
|
|
// reset to the default name.
|
|
func (s *Session) SetTableName(n string) {
|
|
s.tableName = n
|
|
}
|
|
|
|
// validateNewSession checks everything but the session's PublicId
|
|
func (s *Session) validateNewSession(errorPrefix string) error {
|
|
if s.UserId == "" {
|
|
return fmt.Errorf("%s missing user id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.HostId == "" {
|
|
return fmt.Errorf("%s missing host id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.TargetId == "" {
|
|
return fmt.Errorf("%s missing target id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.HostSetId == "" {
|
|
return fmt.Errorf("%s missing host set id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.AuthTokenId == "" {
|
|
return fmt.Errorf("%s missing auth token id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.ScopeId == "" {
|
|
return fmt.Errorf("%s missing scope id: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.Endpoint == "" {
|
|
return fmt.Errorf("%s missing endpoint: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.ExpirationTime.GetTimestamp().AsTime().IsZero() {
|
|
return fmt.Errorf("%s missing expiration time: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.TerminationReason != "" {
|
|
return fmt.Errorf("%s termination reason must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.ServerId != "" {
|
|
return fmt.Errorf("%s server id must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.ServerType != "" {
|
|
return fmt.Errorf("%s server type must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.TofuToken != nil {
|
|
return fmt.Errorf("%s tofu token must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
if s.CtTofuToken != nil {
|
|
return fmt.Errorf("%s ct must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func contains(ss []string, t string) bool {
|
|
for _, s := range ss {
|
|
if strings.EqualFold(s, t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newCert(wrapper wrapping.Wrapper, userId, jobId string, exp time.Time) (ed25519.PrivateKey, []byte, error) {
|
|
if wrapper == nil {
|
|
return nil, nil, fmt.Errorf("new session cert: missing wrapper: %w", db.ErrInvalidParameter)
|
|
}
|
|
if userId == "" {
|
|
return nil, nil, fmt.Errorf("new session cert: missing user id: %w", db.ErrInvalidParameter)
|
|
}
|
|
if jobId == "" {
|
|
return nil, nil, fmt.Errorf("new session cert: missing job id: %w", db.ErrInvalidParameter)
|
|
}
|
|
pubKey, privKey, err := DeriveED25519Key(wrapper, userId, jobId)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("new session cert: ")
|
|
}
|
|
template := &x509.Certificate{
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
DNSNames: []string{jobId},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
|
|
SerialNumber: big.NewInt(mathrand.Int63()),
|
|
NotBefore: time.Now().Add(-1 * time.Minute),
|
|
NotAfter: exp,
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
}
|
|
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("new session cert: %w", err)
|
|
}
|
|
return privKey, certBytes, nil
|
|
}
|
|
|
|
func (s *Session) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
|
if err := structwrapping.WrapStruct(ctx, cipher, s, nil); err != nil {
|
|
return fmt.Errorf("error encrypting session: %w", err)
|
|
}
|
|
s.KeyId = cipher.KeyID()
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
|
if err := structwrapping.UnwrapStruct(ctx, cipher, s, nil); err != nil {
|
|
return fmt.Errorf("error decrypting session: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type sessionView struct {
|
|
// Session fields
|
|
PublicId string `json:"public_id,omitempty" gorm:"primary_key"`
|
|
UserId string `json:"user_id,omitempty" gorm:"default:null"`
|
|
HostId string `json:"host_id,omitempty" gorm:"default:null"`
|
|
ServerId string `json:"server_id,omitempty" gorm:"default:null"`
|
|
ServerType string `json:"server_type,omitempty" gorm:"default:null"`
|
|
TargetId string `json:"target_id,omitempty" gorm:"default:null"`
|
|
HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"`
|
|
AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"`
|
|
ScopeId string `json:"scope_id,omitempty" gorm:"default:null"`
|
|
Certificate []byte `json:"certificate,omitempty" gorm:"default:null"`
|
|
ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"`
|
|
CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"`
|
|
TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"`
|
|
TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"`
|
|
CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
|
UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
|
Version uint32 `json:"version,omitempty" gorm:"default:null"`
|
|
Endpoint string `json:"-" gorm:"default:null"`
|
|
ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"`
|
|
KeyId string `json:"key_id,omitempty" gorm:"not_null"`
|
|
|
|
// State fields
|
|
Status string `json:"state,omitempty" gorm:"column:state"`
|
|
PreviousEndTime *timestamp.Timestamp `json:"previous_end_time,omitempty" gorm:"default:current_timestamp"`
|
|
StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"`
|
|
EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"`
|
|
}
|
|
|
|
// TableName returns the tablename to override the default gorm table name
|
|
func (s *sessionView) TableName() string {
|
|
return "session_with_state"
|
|
}
|