* Update golang.org deps and Go version * Upgrade golangci-lint * all: fix new vet warnings https://github.com/golang/go/issues/60529 introduced a new vet warning for when format strings are used without arguments, which is technically a bug since it shouldn't be using the functions taking format string arguments in those cases.
976 lines
35 KiB
Go
976 lines
35 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"context"
|
|
stderrors "errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/common"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/downstream"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
|
|
intglobals "github.com/hashicorp/boundary/internal/globals"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/server"
|
|
"github.com/hashicorp/boundary/internal/session"
|
|
"github.com/hashicorp/boundary/internal/types/scope"
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
|
|
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
"github.com/hashicorp/go-bexpr"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
)
|
|
|
|
type workerServiceServer struct {
|
|
pbs.UnsafeServerCoordinationServiceServer
|
|
pbs.UnsafeSessionServiceServer
|
|
|
|
serversRepoFn common.ServersRepoFactory
|
|
workerAuthRepoFn common.WorkerAuthRepoStorageFactory
|
|
sessionRepoFn session.RepositoryFactory
|
|
connectionRepoFn common.ConnectionRepoFactory
|
|
downstreams downstream.Graph
|
|
updateTimes *sync.Map
|
|
kms *kms.Kms
|
|
livenessTimeToStale *atomic.Int64
|
|
controllerExt intglobals.ControllerExtension
|
|
}
|
|
|
|
var (
|
|
_ pbs.SessionServiceServer = &workerServiceServer{}
|
|
_ pbs.ServerCoordinationServiceServer = &workerServiceServer{}
|
|
|
|
workerFilterSelectionFn = egressFilterSelector
|
|
// connectionRouteFn returns a route to the egress worker. If the requester
|
|
// is the egress worker a route of length 1 is returned. A route of
|
|
// length 0 is never returned unless there is an error.
|
|
connectionRouteFn = singleHopConnectionRoute
|
|
|
|
// getProtocolContext populates the protocol specific context fields
|
|
// depending on the protocol used to for the boundary connection. Defaults
|
|
// to noProtocolContext since tcp connections are the only protocol schemes
|
|
// available in OSS and are a straight forward proxy with no additional
|
|
// fields needed.
|
|
getProtocolContext = noProtocolContext
|
|
|
|
// updateWorkerStorageBucketCredentialStatesFn will update the worker storage bucket
|
|
// credential state.
|
|
updateWorkerStorageBucketCredentialStatesFn = updateWorkerStorageBucketCredentialStates
|
|
)
|
|
|
|
// singleHopConnectionRoute returns a route consisting of the singlehop worker (the root worker id)
|
|
func singleHopConnectionRoute(_ context.Context, w *server.Worker, _ *session.Session, _ *server.Repository, _ downstream.Graph) ([]string, error) {
|
|
return []string{w.GetPublicId()}, nil
|
|
}
|
|
|
|
func NewWorkerServiceServer(
|
|
serversRepoFn common.ServersRepoFactory,
|
|
workerAuthRepoFn common.WorkerAuthRepoStorageFactory,
|
|
sessionRepoFn session.RepositoryFactory,
|
|
connectionRepoFn common.ConnectionRepoFactory,
|
|
downstreams downstream.Graph,
|
|
updateTimes *sync.Map,
|
|
kms *kms.Kms,
|
|
livenessTimeToStale *atomic.Int64,
|
|
controllerExt intglobals.ControllerExtension,
|
|
) *workerServiceServer {
|
|
return &workerServiceServer{
|
|
serversRepoFn: serversRepoFn,
|
|
workerAuthRepoFn: workerAuthRepoFn,
|
|
sessionRepoFn: sessionRepoFn,
|
|
connectionRepoFn: connectionRepoFn,
|
|
downstreams: downstreams,
|
|
updateTimes: updateTimes,
|
|
kms: kms,
|
|
livenessTimeToStale: livenessTimeToStale,
|
|
controllerExt: controllerExt,
|
|
}
|
|
}
|
|
|
|
func (ws *workerServiceServer) Statistics(ctx context.Context, req *pbs.StatisticsRequest) (*pbs.StatisticsResponse, error) {
|
|
const op = "workers.(workerServiceServer).Statistics"
|
|
workerId := req.GetWorkerId()
|
|
if workerId == "" {
|
|
return &pbs.StatisticsResponse{}, status.Error(codes.InvalidArgument, "worker id is empty")
|
|
}
|
|
connectionRepo, err := ws.connectionRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting connection repo"))
|
|
return &pbs.StatisticsResponse{}, status.Errorf(codes.Internal, "Error acquiring connection repo: %v", err)
|
|
}
|
|
connectionStats := []*session.Connection{}
|
|
for _, s := range req.GetSessions() {
|
|
sessionId := s.GetSessionId()
|
|
if sessionId == "" {
|
|
return &pbs.StatisticsResponse{}, status.Error(codes.InvalidArgument, "session id is empty")
|
|
}
|
|
for _, c := range s.Connections {
|
|
connectionId := c.GetConnectionId()
|
|
if connectionId == "" {
|
|
return &pbs.StatisticsResponse{}, status.Error(codes.InvalidArgument, "connection id is empty")
|
|
}
|
|
switch c.Status {
|
|
case pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED, pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED:
|
|
connectionStats = append(connectionStats, &session.Connection{
|
|
PublicId: c.GetConnectionId(),
|
|
BytesUp: c.GetBytesUp(),
|
|
BytesDown: c.GetBytesDown(),
|
|
})
|
|
default:
|
|
// Other statuses are not included in the stats sent to the domain
|
|
}
|
|
}
|
|
}
|
|
updateBytesErr := session.UpdateConnectionBytesUpDown(ctx, connectionRepo, connectionStats)
|
|
_, closeOrphanedErr := session.CloseOrphanedConnections(ctx, connectionRepo, workerId, connectionStats)
|
|
sessionErrs := stderrors.Join(updateBytesErr, closeOrphanedErr)
|
|
if sessionErrs != nil {
|
|
return &pbs.StatisticsResponse{}, errors.Wrap(ctx, sessionErrs, op)
|
|
}
|
|
return &pbs.StatisticsResponse{}, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) SessionInfo(ctx context.Context, req *pbs.SessionInfoRequest) (*pbs.SessionInfoResponse, error) {
|
|
const op = "workers.(workerServiceServer).SessionInfo"
|
|
workerId := req.GetWorkerId()
|
|
if workerId == "" {
|
|
return &pbs.SessionInfoResponse{}, status.Error(codes.InvalidArgument, "worker id is empty")
|
|
}
|
|
sessionTypeCache := map[string]pbs.SessionType{}
|
|
var recordedSessionIds []string
|
|
var ingressedSessionIds []string
|
|
for _, s := range req.GetSessions() {
|
|
sessionId := s.GetSessionId()
|
|
sessionStatus := s.GetSessionStatus()
|
|
sessionType := s.GetSessionType()
|
|
switch sessionType {
|
|
case pbs.SessionType_SESSION_TYPE_UNSPECIFIED:
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.InvalidArgument, "unspecified session type")
|
|
case pbs.SessionType_SESSION_TYPE_INGRESSED:
|
|
if sessionStatus == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING || sessionStatus == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED {
|
|
// No need to see about canceling anything
|
|
continue
|
|
}
|
|
sessionTypeCache[sessionId] = sessionType
|
|
ingressedSessionIds = append(ingressedSessionIds, sessionId)
|
|
case pbs.SessionType_SESSION_TYPE_RECORDED:
|
|
if sessionStatus == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING || sessionStatus == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED {
|
|
// No need to see about canceling anything
|
|
continue
|
|
}
|
|
sessionTypeCache[sessionId] = sessionType
|
|
recordedSessionIds = append(recordedSessionIds, sessionId)
|
|
default:
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.InvalidArgument, "unknown session type: %q", s.GetSessionType().String())
|
|
}
|
|
}
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err)
|
|
}
|
|
|
|
result := &pbs.SessionInfoResponse{}
|
|
nonActiveSessions, err := sessRepo.CheckIfNotActive(ctx, append(ingressedSessionIds, recordedSessionIds...))
|
|
if err != nil {
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.Internal,
|
|
"Error checking if sessions are no longer active: %v", err)
|
|
}
|
|
for _, s := range nonActiveSessions {
|
|
switch sessionTypeCache[s.SessionId] {
|
|
case pbs.SessionType_SESSION_TYPE_INGRESSED:
|
|
var connChanges []*pbs.Connection
|
|
for _, conn := range s.Connections {
|
|
connChanges = append(connChanges, &pbs.Connection{
|
|
ConnectionId: conn.GetPublicId(),
|
|
Status: session.StatusClosed.ProtoVal(),
|
|
})
|
|
}
|
|
result.NonActiveSessions = append(result.NonActiveSessions, &pbs.Session{
|
|
SessionId: s.SessionId,
|
|
SessionStatus: s.Status.ProtoVal(),
|
|
SessionType: pbs.SessionType_SESSION_TYPE_INGRESSED,
|
|
Connections: connChanges,
|
|
})
|
|
case pbs.SessionType_SESSION_TYPE_RECORDED:
|
|
result.NonActiveSessions = append(result.NonActiveSessions, &pbs.Session{
|
|
SessionId: s.SessionId,
|
|
SessionStatus: s.Status.ProtoVal(),
|
|
SessionType: pbs.SessionType_SESSION_TYPE_RECORDED,
|
|
})
|
|
default:
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.Internal,
|
|
"unknown session type found for non active session: %q", s.SessionId)
|
|
}
|
|
}
|
|
|
|
serverRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to upsert session info status time: %v", err)
|
|
}
|
|
err = serverRepo.UpsertSessionInfo(ctx, workerId)
|
|
if err != nil {
|
|
return &pbs.SessionInfoResponse{}, status.Errorf(codes.Internal, "Error updating the latest status time for the server session info: %v", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Status is the deprecated method for worker status updates.
|
|
// This is safe to remove after the release of Boundary v0.20.0.
|
|
func (ws *workerServiceServer) Status(ctx context.Context, req *pbs.StatusRequest) (*pbs.StatusResponse, error) {
|
|
const op = "workers.(workerServiceServer).Status"
|
|
|
|
wStat := req.GetWorkerStatus()
|
|
if wStat == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "Worker sent nil status.")
|
|
}
|
|
switch {
|
|
case wStat.GetName() == "" && wStat.GetKeyId() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "Name and keyId are not set in the request; one is required.")
|
|
case wStat.GetAddress() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "Address is not set but is required.")
|
|
}
|
|
// This Store call is currently only for testing purposes
|
|
ws.updateTimes.Store(wStat.GetName(), time.Now())
|
|
|
|
serverRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting server repo"))
|
|
return nil, status.Errorf(codes.Internal, "Error acquiring repo to store worker status: %v", err)
|
|
}
|
|
|
|
// Convert API tags to storage tags
|
|
wTags := wStat.GetTags()
|
|
workerTags := make([]*server.Tag, 0, len(wTags))
|
|
for _, v := range wTags {
|
|
workerTags = append(workerTags, &server.Tag{
|
|
Key: v.GetKey(),
|
|
Value: v.GetValue(),
|
|
})
|
|
}
|
|
|
|
if wStat.OperationalState == "" {
|
|
// If this is an older worker (pre 0.11), it will not have ReleaseVersion and we'll default to active.
|
|
// Otherwise, default to Unknown.
|
|
if wStat.ReleaseVersion == "" {
|
|
wStat.OperationalState = server.ActiveOperationalState.String()
|
|
} else {
|
|
wStat.OperationalState = server.UnknownOperationalState.String()
|
|
}
|
|
}
|
|
|
|
if wStat.LocalStorageState == "" {
|
|
// If this is an older worker (pre 0.15), it will not have LocalStorageState as part of it's status
|
|
// so we'll default to unknown.
|
|
wStat.LocalStorageState = server.UnknownLocalStorageState.String()
|
|
}
|
|
|
|
wConf := server.NewWorker(scope.Global.String(),
|
|
server.WithName(wStat.GetName()),
|
|
server.WithDescription(wStat.GetDescription()),
|
|
server.WithAddress(wStat.GetAddress()),
|
|
server.WithWorkerTags(workerTags...),
|
|
server.WithReleaseVersion(wStat.ReleaseVersion),
|
|
server.WithOperationalState(wStat.OperationalState),
|
|
server.WithLocalStorageState(wStat.LocalStorageState))
|
|
opts := []server.Option{server.WithUpdateTags(req.GetUpdateTags())}
|
|
if wStat.GetPublicId() != "" {
|
|
opts = append(opts, server.WithPublicId(wStat.GetPublicId()))
|
|
}
|
|
if wStat.GetKeyId() != "" {
|
|
opts = append(opts, server.WithKeyId(wStat.GetKeyId()))
|
|
}
|
|
workerId, err := serverRepo.UpsertWorkerStatus(ctx, wConf, opts...)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error storing worker status"))
|
|
return nil, status.Errorf(codes.Internal, "Error storing worker status: %v", err)
|
|
}
|
|
|
|
// update storage states
|
|
if sbcStates := wStat.GetStorageBucketCredentialStates(); sbcStates != nil && workerId != "" {
|
|
updateWorkerStorageBucketCredentialStatesFn(ctx, serverRepo, workerId, sbcStates)
|
|
}
|
|
|
|
controllers, err := serverRepo.ListControllers(ctx, server.WithLiveness(time.Duration(ws.livenessTimeToStale.Load())))
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting current controllers"))
|
|
return nil, status.Errorf(codes.Internal, "Error getting current controllers: %v", err)
|
|
}
|
|
|
|
responseControllers := []*pbs.UpstreamServer{}
|
|
for _, c := range controllers {
|
|
thisController := &pbs.UpstreamServer{
|
|
Address: c.Address,
|
|
Type: pbs.UpstreamServer_TYPE_CONTROLLER,
|
|
}
|
|
responseControllers = append(responseControllers, thisController)
|
|
}
|
|
|
|
workerAuthRepo, err := ws.workerAuthRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting worker auth repo"))
|
|
return nil, status.Errorf(codes.Internal, "Error acquiring repo to lookup worker auth info: %v", err)
|
|
}
|
|
|
|
authorizedDownstreams := &pbs.AuthorizedDownstreamWorkerList{}
|
|
if len(req.GetConnectedWorkerPublicIds()) > 0 {
|
|
knownConnectedWorkers, err := serverRepo.VerifyKnownWorkers(ctx, req.GetConnectedWorkerPublicIds())
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting known connected worker ids"))
|
|
return nil, status.Errorf(codes.Internal, "Error getting known connected worker ids: %v", err)
|
|
}
|
|
authorizedDownstreams.WorkerPublicIds = knownConnectedWorkers
|
|
}
|
|
|
|
if len(req.GetConnectedUnmappedWorkerKeyIdentifiers()) > 0 {
|
|
authorizedKeyIds, err := workerAuthRepo.FilterToAuthorizedWorkerKeyIds(ctx, req.GetConnectedUnmappedWorkerKeyIdentifiers())
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting authorized unmapped worker key ids"))
|
|
return nil, status.Errorf(codes.Internal, "Error getting authorized worker key ids: %v", err)
|
|
}
|
|
authorizedDownstreams.UnmappedWorkerKeyIdentifiers = authorizedKeyIds
|
|
}
|
|
|
|
ret := &pbs.StatusResponse{
|
|
CalculatedUpstreams: responseControllers,
|
|
WorkerId: workerId,
|
|
AuthorizedDownstreamWorkers: authorizedDownstreams,
|
|
}
|
|
|
|
stateReport := make([]*session.StateReport, 0, len(req.GetJobs()))
|
|
var monitoredSessionIds []string
|
|
|
|
for _, jobStatus := range req.GetJobs() {
|
|
switch jobStatus.Job.GetType() {
|
|
case pbs.JOBTYPE_JOBTYPE_MONITOR_SESSION:
|
|
si := jobStatus.GetJob().GetMonitorSessionInfo()
|
|
if si == nil {
|
|
return nil, status.Error(codes.Internal, "Error getting monitored session info at status time")
|
|
}
|
|
switch si.Status {
|
|
case pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
|
|
pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED:
|
|
// No need to see about canceling anything
|
|
continue
|
|
}
|
|
|
|
monitoredSessionIds = append(monitoredSessionIds, si.GetSessionId())
|
|
case pbs.JOBTYPE_JOBTYPE_SESSION:
|
|
si := jobStatus.GetJob().GetSessionInfo()
|
|
if si == nil {
|
|
return nil, status.Error(codes.Internal, "Error getting session info at status time")
|
|
}
|
|
|
|
switch si.Status {
|
|
case pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
|
|
pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED:
|
|
// No need to see about canceling anything
|
|
continue
|
|
}
|
|
|
|
sr := &session.StateReport{
|
|
SessionId: si.GetSessionId(),
|
|
Connections: make([]*session.Connection, 0, len(si.GetConnections())),
|
|
}
|
|
for _, conn := range si.GetConnections() {
|
|
switch conn.Status {
|
|
case pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED,
|
|
pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED:
|
|
sr.Connections = append(sr.Connections, &session.Connection{
|
|
PublicId: conn.GetConnectionId(),
|
|
BytesUp: conn.GetBytesUp(),
|
|
BytesDown: conn.GetBytesDown(),
|
|
})
|
|
}
|
|
}
|
|
stateReport = append(stateReport, sr)
|
|
}
|
|
}
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting sessions repo"))
|
|
return nil, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err)
|
|
}
|
|
connectionRepo, err := ws.connectionRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting connection repo"))
|
|
return nil, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err)
|
|
}
|
|
|
|
notActive, err := session.WorkerStatusReport(ctx, sessRepo, connectionRepo, workerId, stateReport)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal,
|
|
"Error comparing state of sessions for worker with public id %q: %v",
|
|
workerId, err)
|
|
}
|
|
for _, na := range notActive {
|
|
var connChanges []*pbs.Connection
|
|
for _, conn := range na.Connections {
|
|
connChanges = append(connChanges, &pbs.Connection{
|
|
ConnectionId: conn.GetPublicId(),
|
|
Status: session.StatusClosed.ProtoVal(),
|
|
})
|
|
}
|
|
processErr := pbs.SessionProcessingError_SESSION_PROCESSING_ERROR_UNSPECIFIED
|
|
if na.Unrecognized {
|
|
processErr = pbs.SessionProcessingError_SESSION_PROCESSING_ERROR_UNRECOGNIZED
|
|
}
|
|
ret.JobsRequests = append(ret.JobsRequests, &pbs.JobChangeRequest{
|
|
Job: &pbs.Job{
|
|
Type: pbs.JOBTYPE_JOBTYPE_SESSION,
|
|
JobInfo: &pbs.Job_SessionInfo{
|
|
SessionInfo: &pbs.SessionJobInfo{
|
|
SessionId: na.SessionId,
|
|
Status: na.Status.ProtoVal(),
|
|
Connections: connChanges,
|
|
ProcessingError: processErr,
|
|
},
|
|
},
|
|
},
|
|
RequestType: pbs.CHANGETYPE_CHANGETYPE_UPDATE_STATE,
|
|
})
|
|
}
|
|
|
|
nonActiveMonitoredSessions, err := sessRepo.CheckIfNotActive(ctx, monitoredSessionIds)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal,
|
|
"Error checking if monitored jobs are no longer active: %v", err)
|
|
}
|
|
for _, na := range nonActiveMonitoredSessions {
|
|
processErr := pbs.SessionProcessingError_SESSION_PROCESSING_ERROR_UNSPECIFIED
|
|
if na.Unrecognized {
|
|
processErr = pbs.SessionProcessingError_SESSION_PROCESSING_ERROR_UNRECOGNIZED
|
|
}
|
|
ret.JobsRequests = append(ret.JobsRequests, &pbs.JobChangeRequest{
|
|
Job: &pbs.Job{
|
|
Type: pbs.JOBTYPE_JOBTYPE_MONITOR_SESSION,
|
|
JobInfo: &pbs.Job_MonitorSessionInfo{
|
|
MonitorSessionInfo: &pbs.MonitorSessionJobInfo{
|
|
SessionId: na.SessionId,
|
|
Status: na.Status.ProtoVal(),
|
|
ProcessingError: processErr,
|
|
},
|
|
},
|
|
},
|
|
RequestType: pbs.CHANGETYPE_CHANGETYPE_UPDATE_STATE,
|
|
})
|
|
}
|
|
|
|
err = serverRepo.UpsertSessionInfo(ctx, workerId)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error updating the latest status time for the server session info: %v", err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// ListHcpbWorkers looks up workers that are HCP Boundary-managed.
|
|
func (ws *workerServiceServer) ListHcpbWorkers(ctx context.Context, req *pbs.ListHcpbWorkersRequest) (*pbs.ListHcpbWorkersResponse, error) {
|
|
const op = "workers.(workerServiceServer).ListHcpbWorkers"
|
|
|
|
serversRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error getting servers repo: %v", err)
|
|
}
|
|
|
|
// We use the livenessTimeToStale here instead of WorkerRPCGracePeriod
|
|
// since WorkerRPCGracePeriod is more for deciding which workers should
|
|
// be used for session proxying, but here we care about providing the BYOW
|
|
// workers with a list of which upstreams to connect to as their upstreams.
|
|
managed, err := serversRepo.ListHcpbManagedWorkers(ctx, time.Duration(ws.livenessTimeToStale.Load()))
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error looking up hcpb managed workers: %v", err)
|
|
}
|
|
|
|
resp := &pbs.ListHcpbWorkersResponse{}
|
|
if len(managed) == 0 {
|
|
return resp, nil
|
|
}
|
|
|
|
resp.Workers = make([]*pbs.WorkerInfo, 0, len(managed))
|
|
for _, worker := range managed {
|
|
resp.Workers = append(resp.Workers, &pbs.WorkerInfo{
|
|
Id: worker.PublicId,
|
|
Address: worker.Address,
|
|
})
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) RoutingInfo(ctx context.Context, req *pbs.RoutingInfoRequest) (*pbs.RoutingInfoResponse, error) {
|
|
const op = "workers.(workerServiceServer).RoutingInfo"
|
|
wStat := req.GetWorkerStatus()
|
|
if wStat == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "worker status is required")
|
|
}
|
|
switch {
|
|
case wStat.GetPublicId() == "" && wStat.GetKeyId() == "" && wStat.GetName() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "public id, key id and name are not set in the request; one is required")
|
|
case wStat.GetAddress() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "address is not set but is required")
|
|
case wStat.GetReleaseVersion() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "release version is not set but is required")
|
|
case wStat.GetOperationalState() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "operational state is not set but is required")
|
|
case wStat.GetLocalStorageState() == "":
|
|
return nil, status.Error(codes.InvalidArgument, "local storage state is not set but is required")
|
|
}
|
|
// This Store call is currently only for testing purposes
|
|
ws.updateTimes.Store(wStat.GetName(), time.Now())
|
|
|
|
serverRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting server repo: %v", err)
|
|
}
|
|
workerAuthRepo, err := ws.workerAuthRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting worker auth repo: %v", err)
|
|
}
|
|
|
|
// Convert API tags to storage tags
|
|
workerTags := make([]*server.Tag, 0, len(wStat.GetTags()))
|
|
for _, v := range wStat.GetTags() {
|
|
workerTags = append(workerTags, &server.Tag{
|
|
Key: v.GetKey(),
|
|
Value: v.GetValue(),
|
|
})
|
|
}
|
|
|
|
wConf := server.NewWorker(
|
|
scope.Global.String(),
|
|
server.WithName(wStat.GetName()),
|
|
server.WithDescription(wStat.GetDescription()),
|
|
server.WithAddress(wStat.GetAddress()),
|
|
server.WithWorkerTags(workerTags...),
|
|
server.WithReleaseVersion(wStat.ReleaseVersion),
|
|
server.WithOperationalState(wStat.OperationalState),
|
|
server.WithLocalStorageState(wStat.LocalStorageState),
|
|
)
|
|
opts := []server.Option{server.WithUpdateTags(req.GetUpdateTags())}
|
|
if wStat.GetPublicId() != "" {
|
|
opts = append(opts, server.WithPublicId(wStat.GetPublicId()))
|
|
}
|
|
if wStat.GetKeyId() != "" {
|
|
opts = append(opts, server.WithKeyId(wStat.GetKeyId()))
|
|
}
|
|
workerId, err := serverRepo.UpsertWorkerStatus(ctx, wConf, opts...)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error storing worker status: %v", err)
|
|
}
|
|
|
|
if sbcStates := wStat.GetStorageBucketCredentialStates(); sbcStates != nil {
|
|
updateWorkerStorageBucketCredentialStatesFn(ctx, serverRepo, workerId, sbcStates)
|
|
}
|
|
|
|
controllers, err := serverRepo.ListControllers(ctx, server.WithLiveness(time.Duration(ws.livenessTimeToStale.Load())))
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error getting current controllers: %v", err)
|
|
}
|
|
var upstreamAddresses []string
|
|
for _, c := range controllers {
|
|
upstreamAddresses = append(upstreamAddresses, c.Address)
|
|
}
|
|
|
|
authorizedDownstreams, err := server.VerifyKnownAndUnmappedWorkers(
|
|
ctx,
|
|
serverRepo,
|
|
workerAuthRepo,
|
|
req.GetConnectedWorkerPublicIds(),
|
|
req.GetConnectedUnmappedWorkerKeyIdentifiers(),
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error getting known and authorized workers: %v", err)
|
|
}
|
|
|
|
ret := &pbs.RoutingInfoResponse{
|
|
WorkerId: workerId,
|
|
CalculatedUpstreamAddresses: upstreamAddresses,
|
|
AuthorizedDownstreamWorkers: &pbs.AuthorizedDownstreamWorkerList{
|
|
WorkerPublicIds: authorizedDownstreams.WorkerPublicIds,
|
|
UnmappedWorkerKeyIdentifiers: authorizedDownstreams.UnmappedWorkerKeyIds,
|
|
},
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Single-hop filter lookup. We have either an egress filter or worker filter to use, if any
|
|
// Used to verify that the worker serving this session to a client matches this filter
|
|
func egressFilterSelector(sessionInfo *session.Session) string {
|
|
if sessionInfo.EgressWorkerFilter != "" {
|
|
return sessionInfo.EgressWorkerFilter
|
|
} else if sessionInfo.WorkerFilter != "" {
|
|
return sessionInfo.WorkerFilter
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// noProtocolContext doesn't provide any protocol context since tcp doesn't need any
|
|
func noProtocolContext(
|
|
context.Context,
|
|
*session.Repository,
|
|
*server.Repository,
|
|
common.WorkerAuthRepoStorageFactory,
|
|
*pbs.AuthorizeConnectionRequest,
|
|
[]string,
|
|
string,
|
|
intglobals.ControllerExtension,
|
|
) (*anypb.Any, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func lookupSessionWorkerFilter(ctx context.Context, sessionInfo *session.Session, authzSummary *session.AuthzSummary, ws *workerServiceServer,
|
|
req *pbs.LookupSessionRequest,
|
|
) error {
|
|
const op = "workers.lookupSessionEgressWorkerFilter"
|
|
|
|
serversRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error getting server repo"))
|
|
return status.Errorf(codes.Internal, "Error acquiring server repo when looking up session: %v", err)
|
|
}
|
|
w, err := serversRepo.LookupWorker(ctx, req.GetWorkerId())
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up worker", "worker_id", req.WorkerId))
|
|
return status.Errorf(codes.Internal, "Error looking up worker: %v", err)
|
|
}
|
|
if w == nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up worker", "worker_id", req.WorkerId))
|
|
return status.Errorf(codes.Internal, "Worker not found")
|
|
}
|
|
|
|
filter := workerFilterSelectionFn(sessionInfo)
|
|
if filter == "" {
|
|
// Verify that this ingress worker can build a route to the endpoint safely
|
|
// While the AuthorizeSession may have done a similar check, this makes sure
|
|
// we can select a worker for egress that wouldn't potentially grant access
|
|
// to a private ip address in the network of the boundary deployment in the
|
|
// case of hcp.
|
|
if _, err := connectionRouteFn(ctx, w, sessionInfo, serversRepo, ws.downstreams); err != nil {
|
|
return status.Errorf(codes.Internal, "error calculating route to endpoint: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Build the map for filtering.
|
|
tagMap := w.CanonicalTags()
|
|
|
|
// Create the evaluator
|
|
eval, err := bexpr.CreateEvaluator(filter)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error creating worker filter evaluator", "worker_id", req.WorkerId))
|
|
return status.Errorf(codes.Internal, "Error creating worker filter evaluator: %v", err)
|
|
}
|
|
filterInput := map[string]interface{}{
|
|
"name": w.GetName(),
|
|
"tags": tagMap,
|
|
}
|
|
ok, err := eval.Evaluate(filterInput)
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "Worker filter expression evaluation resulted in error: %s", err)
|
|
}
|
|
if !ok {
|
|
return handlers.ApiErrorWithCodeAndMessage(codes.FailedPrecondition, "Worker filter expression precludes this worker from serving this session")
|
|
}
|
|
|
|
// Verify that this ingress worker can build a route to the endpoint safely
|
|
// While the AuthorizeSession may have done a similar check, this makes sure
|
|
// we can select a worker for egress that wouldn't potentially grant access
|
|
// to a private ip address in the network of the boundary deployment in the
|
|
// case of hcp.
|
|
if _, err = connectionRouteFn(ctx, w, sessionInfo, serversRepo, ws.downstreams); err != nil {
|
|
return status.Errorf(codes.Internal, "error calculating route to endpoint: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) LookupSession(ctx context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
|
|
const op = "workers.(workerServiceServer).LookupSession"
|
|
|
|
if req.WorkerId == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "Did not receive worker id when looking up session")
|
|
}
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error getting session repo: %v", err)
|
|
}
|
|
|
|
sessionInfo, authzSummary, err := sessRepo.LookupSession(ctx, req.GetSessionId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "Error looking up session: %v", err)
|
|
}
|
|
if sessionInfo == nil {
|
|
return nil, status.Error(codes.PermissionDenied, "Unknown session ID.")
|
|
}
|
|
if len(sessionInfo.States) == 0 {
|
|
return nil, status.Error(codes.Internal, "Empty session states during lookup.")
|
|
}
|
|
|
|
err = lookupSessionWorkerFilter(ctx, sessionInfo, authzSummary, ws, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
creds, err := sessRepo.ListSessionCredentials(ctx, sessionInfo.ProjectId, sessionInfo.PublicId)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal,
|
|
"Error retrieving session credentials: %s", err)
|
|
}
|
|
var workerCreds []*pbs.Credential
|
|
for _, c := range creds {
|
|
m := &pbs.Credential{}
|
|
err = proto.Unmarshal(c, m)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal,
|
|
"Error unmarshaling credentials: %s", err)
|
|
}
|
|
workerCreds = append(workerCreds, m)
|
|
}
|
|
|
|
resp := &pbs.LookupSessionResponse{
|
|
Authorization: &targets.SessionAuthorizationData{
|
|
SessionId: sessionInfo.GetPublicId(),
|
|
Certificate: sessionInfo.Certificate,
|
|
PrivateKey: sessionInfo.CertificatePrivateKey,
|
|
},
|
|
Status: sessionInfo.States[0].Status.ProtoVal(),
|
|
Version: sessionInfo.Version,
|
|
TofuToken: string(sessionInfo.TofuToken),
|
|
Endpoint: sessionInfo.Endpoint,
|
|
Expiration: sessionInfo.ExpirationTime.Timestamp,
|
|
ConnectionLimit: sessionInfo.ConnectionLimit,
|
|
ConnectionsLeft: authzSummary.ConnectionLimit,
|
|
HostId: sessionInfo.HostId,
|
|
HostSetId: sessionInfo.HostSetId,
|
|
TargetId: sessionInfo.TargetId,
|
|
UserId: sessionInfo.UserId,
|
|
Credentials: workerCreds,
|
|
}
|
|
if resp.ConnectionsLeft != -1 {
|
|
resp.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) {
|
|
const op = "workers.(workerServiceServer).CancelSession"
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
ses, _, err := sessRepo.LookupSession(ctx, req.GetSessionId())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ses == nil {
|
|
return nil, status.Error(codes.PermissionDenied, "Unknown session ID.")
|
|
}
|
|
|
|
ses, err = sessRepo.CancelSession(ctx, req.GetSessionId(), ses.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pbs.CancelSessionResponse{
|
|
Status: ses.States[0].Status.ProtoVal(),
|
|
}, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) ActivateSession(ctx context.Context, req *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) {
|
|
const op = "workers.(workerServiceServer).ActivateSession"
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
sessionInfo, sessionStates, err := sessRepo.ActivateSession(ctx, req.GetSessionId(), req.GetVersion(), []byte(req.GetTofuToken()))
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error looking up session: %v", err)
|
|
}
|
|
if sessionInfo == nil {
|
|
return nil, status.Error(codes.PermissionDenied, "Unknown session ID.")
|
|
}
|
|
if len(sessionStates) == 0 {
|
|
return nil, status.Error(codes.Internal, "Invalid session state in activate response.")
|
|
}
|
|
|
|
return &pbs.ActivateSessionResponse{
|
|
Status: sessionStates[0].Status.ProtoVal(),
|
|
}, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
|
|
const op = "workers.(workerServiceServer).AuthorizeConnection"
|
|
connectionRepo, err := ws.connectionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
sessionRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
serversRepo, err := ws.serversRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting server repo: %v", err)
|
|
}
|
|
w, err := serversRepo.LookupWorker(ctx, req.GetWorkerId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error looking up worker: %v", err)
|
|
}
|
|
if w == nil {
|
|
return nil, status.Errorf(codes.NotFound, "worker not found with name %q", req.GetWorkerId())
|
|
}
|
|
|
|
connectionInfo, err := connectionRepo.AuthorizeConnection(ctx, req.GetSessionId(), w.GetPublicId())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if connectionInfo == nil {
|
|
return nil, status.Error(codes.Internal, "Invalid authorize connection response.")
|
|
}
|
|
|
|
sessInfo, authzSummary, err := sessionRepo.LookupSession(ctx, req.GetSessionId())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sessInfo == nil {
|
|
return nil, status.Errorf(codes.Internal, "Invalid session info in lookup session response")
|
|
}
|
|
|
|
route, err := connectionRouteFn(ctx, w, sessInfo, serversRepo, ws.downstreams)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting route to egress worker: %v", err)
|
|
}
|
|
|
|
ret := &pbs.AuthorizeConnectionResponse{
|
|
ConnectionId: connectionInfo.GetPublicId(),
|
|
Status: session.ConnectionStatusFromString(connectionInfo.Status).ProtoVal(),
|
|
ConnectionsLeft: authzSummary.ConnectionLimit,
|
|
Route: route,
|
|
}
|
|
if pc, err := getProtocolContext(
|
|
ctx,
|
|
sessionRepo,
|
|
serversRepo,
|
|
ws.workerAuthRepoFn,
|
|
req,
|
|
route,
|
|
ret.ConnectionId,
|
|
ws.controllerExt,
|
|
); err != nil {
|
|
return nil, err
|
|
} else {
|
|
ret.ProtocolContext = pc
|
|
}
|
|
if ret.ConnectionsLeft != -1 {
|
|
ret.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) {
|
|
const op = "workers.(workerServiceServer).ConnectConnection"
|
|
connRepo, err := ws.connectionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
connectionInfo, err := connRepo.ConnectConnection(ctx, session.ConnectWith{
|
|
ConnectionId: req.GetConnectionId(),
|
|
ClientTcpAddress: req.GetClientTcpAddress(),
|
|
ClientTcpPort: req.GetClientTcpPort(),
|
|
EndpointTcpAddress: req.GetEndpointTcpAddress(),
|
|
EndpointTcpPort: req.GetEndpointTcpPort(),
|
|
UserClientIp: req.GetUserClientIp(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if connectionInfo == nil {
|
|
return nil, status.Error(codes.Internal, "Invalid connect connection response.")
|
|
}
|
|
|
|
return &pbs.ConnectConnectionResponse{
|
|
Status: session.ConnectionStatusFromString(connectionInfo.Status).ProtoVal(),
|
|
}, nil
|
|
}
|
|
|
|
func (ws *workerServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
|
|
const op = "workers.(workerServiceServer).CloseConnection"
|
|
numCloses := len(req.GetCloseRequestData())
|
|
if numCloses == 0 {
|
|
return &pbs.CloseConnectionResponse{}, nil
|
|
}
|
|
|
|
closeWiths := make([]session.CloseWith, 0, numCloses)
|
|
closeIds := make([]string, 0, numCloses)
|
|
|
|
for _, v := range req.GetCloseRequestData() {
|
|
closeIds = append(closeIds, v.GetConnectionId())
|
|
closeWiths = append(closeWiths, session.CloseWith{
|
|
ConnectionId: v.GetConnectionId(),
|
|
BytesUp: v.GetBytesUp(),
|
|
BytesDown: v.GetBytesDown(),
|
|
ClosedReason: session.ClosedReason(v.GetReason()),
|
|
})
|
|
}
|
|
connRepo, err := ws.connectionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting connection repo: %v", err)
|
|
}
|
|
|
|
sessRepo, err := ws.sessionRepoFn()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
|
|
}
|
|
|
|
closeInfos, err := session.CloseConnections(ctx, sessRepo, connRepo, closeWiths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if closeInfos == nil {
|
|
return nil, status.Error(codes.Internal, "Invalid close connection response.")
|
|
}
|
|
|
|
closeData := make([]*pbs.CloseConnectionResponseData, 0, numCloses)
|
|
for _, v := range closeInfos {
|
|
if v.Connection == nil {
|
|
return nil, status.Errorf(codes.Internal, "No connection found while closing one of the connection IDs: %v", closeIds)
|
|
}
|
|
closeData = append(closeData, &pbs.CloseConnectionResponseData{
|
|
ConnectionId: v.Connection.GetPublicId(),
|
|
Status: v.ConnectionState.ProtoVal(),
|
|
})
|
|
}
|
|
|
|
ret := &pbs.CloseConnectionResponse{
|
|
CloseResponseData: closeData,
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func updateWorkerStorageBucketCredentialStates(ctx context.Context, repo *server.Repository, workerId string, states map[string]*plugin.StorageBucketCredentialState) {
|
|
const op = "handlers.updateWorkerStorageBucketCredentialStates"
|
|
return
|
|
}
|