Files
threshold/internal/servers/controller/handlers/targets/target_service.go
T

1059 lines
36 KiB
Go

package targets
import (
"context"
stderrors "errors"
"fmt"
"math/rand"
"net/url"
"strings"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/targets"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/servers"
"github.com/hashicorp/boundary/internal/servers/controller/common"
"github.com/hashicorp/boundary/internal/servers/controller/common/scopeids"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/store"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/sdk/strutil"
"github.com/hashicorp/go-bexpr"
"github.com/mr-tron/base58"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var (
maskManager handlers.MaskManager
// IdActions contains the set of actions that can be performed on
// individual resources
IdActions = action.ActionSet{
action.Read,
action.Update,
action.Delete,
action.AddHostSets,
action.SetHostSets,
action.RemoveHostSets,
action.AuthorizeSession,
}
// CollectionActions contains the set of actions that can be performed on
// this collection
CollectionActions = action.ActionSet{
action.Create,
action.List,
}
)
func init() {
var err error
if maskManager, err = handlers.NewMaskManager(&store.TcpTarget{}, &pb.Target{}, &pb.TcpTargetAttributes{}); err != nil {
panic(err)
}
}
// Service handles request as described by the pbs.TargetServiceServer interface.
type Service struct {
pbs.UnimplementedTargetServiceServer
repoFn common.TargetRepoFactory
iamRepoFn common.IamRepoFactory
serversRepoFn common.ServersRepoFactory
sessionRepoFn common.SessionRepoFactory
staticHostRepoFn common.StaticRepoFactory
kmsCache *kms.Kms
}
// NewService returns a target service which handles target related requests to boundary.
func NewService(
kmsCache *kms.Kms,
repoFn common.TargetRepoFactory,
iamRepoFn common.IamRepoFactory,
serversRepoFn common.ServersRepoFactory,
sessionRepoFn common.SessionRepoFactory,
staticHostRepoFn common.StaticRepoFactory) (Service, error) {
if repoFn == nil {
return Service{}, fmt.Errorf("nil target repository provided")
}
if iamRepoFn == nil {
return Service{}, fmt.Errorf("nil iam repository provided")
}
if serversRepoFn == nil {
return Service{}, fmt.Errorf("nil servers repository provided")
}
if sessionRepoFn == nil {
return Service{}, fmt.Errorf("nil session repository provided")
}
if staticHostRepoFn == nil {
return Service{}, fmt.Errorf("nil static host repository provided")
}
return Service{
repoFn: repoFn,
iamRepoFn: iamRepoFn,
serversRepoFn: serversRepoFn,
sessionRepoFn: sessionRepoFn,
staticHostRepoFn: staticHostRepoFn,
kmsCache: kmsCache,
}, nil
}
var _ pbs.TargetServiceServer = Service{}
// ListTargets implements the interface pbs.TargetServiceServer.
func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) (*pbs.ListTargetsResponse, error) {
if err := validateListRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
if authResults.Error != nil {
return nil, authResults.Error
}
scopeIds, scopeInfoMap, err := scopeids.GetScopeIds(
ctx, s.iamRepoFn, authResults, req.GetScopeId(), req.GetRecursive())
if err != nil {
return nil, err
}
ul, err := s.listFromRepo(ctx, scopeIds)
if err != nil {
return nil, err
}
finalItems := make([]*pb.Target, 0, len(ul))
res := &perms.Resource{
Type: resource.Target,
}
for _, item := range ul {
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
finalItems = append(finalItems, item)
}
}
return &pbs.ListTargetsResponse{Items: finalItems}, nil
}
// GetTargets implements the interface pbs.TargetServiceServer.
func (s Service) GetTarget(ctx context.Context, req *pbs.GetTargetRequest) (*pbs.GetTargetResponse, error) {
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.getFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.GetTargetResponse{Item: u}, nil
}
// CreateTarget implements the interface pbs.TargetServiceServer.
func (s Service) CreateTarget(ctx context.Context, req *pbs.CreateTargetRequest) (*pbs.CreateTargetResponse, error) {
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.createInRepo(ctx, req.GetItem())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.CreateTargetResponse{Item: u, Uri: fmt.Sprintf("targets/%s", u.GetId())}, nil
}
// UpdateTarget implements the interface pbs.TargetServiceServer.
func (s Service) UpdateTarget(ctx context.Context, req *pbs.UpdateTargetRequest) (*pbs.UpdateTargetResponse, error) {
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.UpdateTargetResponse{Item: u}, nil
}
// DeleteTarget implements the interface pbs.TargetServiceServer.
func (s Service) DeleteTarget(ctx context.Context, req *pbs.DeleteTargetRequest) (*pbs.DeleteTargetResponse, error) {
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
if authResults.Error != nil {
return nil, authResults.Error
}
_, err := s.deleteFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
return &pbs.DeleteTargetResponse{}, nil
}
// AddTargetHostSets implements the interface pbs.TargetServiceServer.
func (s Service) AddTargetHostSets(ctx context.Context, req *pbs.AddTargetHostSetsRequest) (*pbs.AddTargetHostSetsResponse, error) {
if err := validateAddRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddHostSets)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.addInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.AddTargetHostSetsResponse{Item: u}, nil
}
// SetTargetHostSets implements the interface pbs.TargetServiceServer.
func (s Service) SetTargetHostSets(ctx context.Context, req *pbs.SetTargetHostSetsRequest) (*pbs.SetTargetHostSetsResponse, error) {
if err := validateSetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetHostSets)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.setInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.SetTargetHostSetsResponse{Item: u}, nil
}
// RemoveTargetHostSets implements the interface pbs.TargetServiceServer.
func (s Service) RemoveTargetHostSets(ctx context.Context, req *pbs.RemoveTargetHostSetsRequest) (*pbs.RemoveTargetHostSetsResponse, error) {
if err := validateRemoveRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveHostSets)
if authResults.Error != nil {
return nil, authResults.Error
}
u, err := s.removeInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion())
if err != nil {
return nil, err
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.RemoveTargetHostSetsResponse{Item: u}, nil
}
func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSessionRequest) (*pbs.AuthorizeSessionResponse, error) {
if err := validateAuthorizeSessionRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AuthorizeSession,
target.WithName(req.GetName()),
target.WithScopeId(req.GetScopeId()),
target.WithScopeName(req.GetScopeName()),
)
if authResults.Error != nil {
return nil, authResults.Error
}
if authResults.RoundTripValue == nil {
return nil, stderrors.New("authorize session: expected to get a target back from auth results")
}
t, ok := authResults.RoundTripValue.(target.Target)
if !ok {
return nil, stderrors.New("authorize session: round tripped auth results value is not a target")
}
if t == nil {
return nil, stderrors.New("authorize session: round tripped target is nil")
}
// This could happen if, say, u_recovery was used or u_anon was granted. But
// don't allow it. It's one thing if grants give access to resources within
// Boundary, even if those could eventually be used to provide an unintended
// user access to a remote system. It's quite another to enable anonymous
// access directly to a remote system.
//
// Note that even if u_anon or u_auth are given grants we can still validate
// a token! So this is just checking that a valid token was provided. The
// actual reality of this works out to excluding:
//
// * True anonymous access (no token provided and u_anon)
//
// * u_recovery access (which is fine, recovery is meant for recovering
// system state, no real reason to allow it to then connect to systems)
if authResults.AuthTokenId == "" {
return nil, handlers.ForbiddenError()
}
// Get the target information
repo, err := s.repoFn()
if err != nil {
return nil, err
}
t, hostSets, err := repo.LookupTarget(ctx, t.GetPublicId())
if err != nil {
if errors.IsNotFoundError(err) {
return nil, handlers.NotFoundErrorf("Target %q not found.", t.GetPublicId())
}
return nil, err
}
if t == nil {
return nil, handlers.NotFoundErrorf("Target %q not found.", t.GetPublicId())
}
// Instantiate some repos
sessionRepo, err := s.sessionRepoFn()
if err != nil {
return nil, err
}
serversRepo, err := s.serversRepoFn()
if err != nil {
return nil, err
}
// First, fetch all available hosts. Unless one was chosen in the request,
// we will pick one at random.
type compoundHost struct {
hostSetId string
hostId string
}
var chosenId *compoundHost
requestedId := req.GetHostId()
staticHostRepo, err := s.staticHostRepoFn()
if err != nil {
return nil, err
}
hostIds := make([]compoundHost, 0, len(hostSets)*10)
HostSetIterationLoop:
for _, tSet := range hostSets {
hsId := tSet.PublicId
switch host.SubtypeFromId(hsId) {
case host.StaticSubtype:
_, hosts, err := staticHostRepo.LookupSet(ctx, hsId)
if err != nil {
return nil, err
}
for _, host := range hosts {
compoundId := compoundHost{hostSetId: hsId, hostId: host.PublicId}
hostIds = append(hostIds, compoundId)
if host.PublicId == requestedId {
chosenId = &compoundId
break HostSetIterationLoop
}
}
}
}
if requestedId != "" && chosenId == nil {
// We didn't find it
return nil, handlers.InvalidArgumentErrorf(
"Errors in provided fields.",
map[string]string{
"host_id": "The requested host id is not available.",
})
}
if chosenId == nil {
if len(hostIds) == 0 {
// No hosts were found, error
return nil, handlers.NotFoundErrorf("No hosts found from available target host sets.")
}
chosenId = &hostIds[rand.Intn(len(hostIds))]
}
// Generate the endpoint URL
endpointUrl := &url.URL{
Scheme: t.GetType(),
}
defaultPort := t.GetDefaultPort()
var endpointHost string
switch host.SubtypeFromId(chosenId.hostId) {
case host.StaticSubtype:
h, err := staticHostRepo.LookupHost(ctx, chosenId.hostId)
if err != nil {
return nil, fmt.Errorf("error looking up host: %w", err)
}
endpointHost = h.Address
if endpointHost == "" {
return nil, stderrors.New("host had empty address")
}
}
if defaultPort != 0 {
endpointUrl.Host = fmt.Sprintf("%s:%d", endpointHost, defaultPort)
} else {
endpointUrl.Host = endpointHost
}
expTime := timestamppb.Now()
expTime.Seconds += int64(t.GetSessionMaxSeconds())
sessionComposition := session.ComposedOf{
UserId: authResults.UserId,
HostId: chosenId.hostId,
TargetId: t.GetPublicId(),
HostSetId: chosenId.hostSetId,
AuthTokenId: authResults.AuthTokenId,
ScopeId: authResults.Scope.Id,
Endpoint: endpointUrl.String(),
ExpirationTime: &timestamp.Timestamp{Timestamp: expTime},
ConnectionLimit: t.GetSessionConnectionLimit(),
WorkerFilter: t.GetWorkerFilter(),
}
sess, err := session.New(sessionComposition)
if err != nil {
return nil, err
}
wrapper, err := s.kmsCache.GetWrapper(ctx, authResults.Scope.Id, kms.KeyPurposeSessions)
if err != nil {
return nil, err
}
sess, privKey, err := sessionRepo.CreateSession(ctx, wrapper, sess)
if err != nil {
return nil, err
}
// WorkerInfo only contains the address; worker IDs below is used to contain
// their IDs in the same order. This is used to fetch tags for filtering.
// But we avoid allocation unless we actually need it.
var workers []*pb.WorkerInfo
var workerIds []string
hasWorkerFilter := len(t.GetWorkerFilter()) > 0
servers, err := serversRepo.ListServers(ctx, servers.ServerTypeWorker)
if err != nil {
return nil, err
}
for _, v := range servers {
if hasWorkerFilter {
workerIds = append(workerIds, v.GetPrivateId())
}
workers = append(workers, &pb.WorkerInfo{Address: v.Address})
}
if hasWorkerFilter && len(workerIds) > 0 {
finalWorkers := make([]*pb.WorkerInfo, 0, len(workers))
// Fetch the tags for the given worker IDs
tags, err := serversRepo.ListTagsForServers(ctx, workerIds)
if err != nil {
return nil, err
}
// Build the map for filtering. This is similar to the filter map we
// built from the worker config, but with one extra level: a map of the
// worker's ID to its filter map.
tagMap := make(map[string]map[string][]string)
for _, tag := range tags {
currWorkerMap := tagMap[tag.ServerId]
if currWorkerMap == nil {
currWorkerMap = make(map[string][]string)
tagMap[tag.ServerId] = currWorkerMap
}
currWorkerMap[tag.Key] = append(currWorkerMap[tag.Key], tag.Value)
// We don't need to reinsert after the fact because maps are
// reference types, so we don't need to re-insert into tagMap
}
// Create the evaluator
eval, err := bexpr.CreateEvaluator(t.GetWorkerFilter())
if err != nil {
return nil, err
}
// Iterate through the known worker IDs, and evaluate. If evaluation
// returns true, add to the final worker slice, which is assigned back
// to workers after this.
for i, worker := range workerIds {
filterInput := map[string]interface{}{
"name": worker,
"tags": tagMap[worker],
}
ok, err := eval.Evaluate(filterInput)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(
codes.FailedPrecondition,
fmt.Sprintf("Worker filter expression evaluation resulted in error: %s", err))
}
if ok {
finalWorkers = append(finalWorkers, workers[i])
}
}
workers = finalWorkers
}
if len(workers) == 0 {
return nil, handlers.ApiErrorWithCodeAndMessage(
codes.FailedPrecondition,
"No workers are available to handle this session, or all have been filtered.")
}
sad := &pb.SessionAuthorizationData{
SessionId: sess.PublicId,
TargetId: t.GetPublicId(),
Scope: authResults.Scope,
CreatedTime: sess.CreateTime.GetTimestamp(),
Type: t.GetType(),
Certificate: sess.Certificate,
PrivateKey: privKey,
HostId: chosenId.hostId,
Endpoint: endpointUrl.String(),
WorkerInfo: workers,
ConnectionLimit: t.GetSessionConnectionLimit(),
}
marshaledSad, err := proto.Marshal(sad)
if err != nil {
return nil, err
}
encodedMarshaledSad := base58.FastBase58Encoding(marshaledSad)
ret := &pb.SessionAuthorization{
SessionId: sess.PublicId,
TargetId: t.GetPublicId(),
Scope: authResults.Scope,
CreatedTime: sess.CreateTime.GetTimestamp(),
Type: t.GetType(),
AuthorizationToken: string(encodedMarshaledSad),
UserId: authResults.UserId,
HostId: chosenId.hostId,
HostSetId: chosenId.hostSetId,
Endpoint: endpointUrl.String(),
}
return &pbs.AuthorizeSessionResponse{Item: ret}, nil
}
func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
u, m, err := repo.LookupTarget(ctx, id)
if err != nil {
if errors.IsNotFoundError(err) {
return nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id)
}
return nil, err
}
if u == nil {
return nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id)
}
return toProto(u, m)
}
func (s Service) createInRepo(ctx context.Context, item *pb.Target) (*pb.Target, error) {
opts := []target.Option{target.WithName(item.GetName().GetValue())}
if item.GetDescription() != nil {
opts = append(opts, target.WithDescription(item.GetDescription().GetValue()))
}
if item.GetSessionMaxSeconds() != nil {
opts = append(opts, target.WithSessionMaxSeconds(item.GetSessionMaxSeconds().GetValue()))
}
if item.GetSessionConnectionLimit() != nil {
opts = append(opts, target.WithSessionConnectionLimit(item.GetSessionConnectionLimit().GetValue()))
}
if item.GetWorkerFilter() != nil {
opts = append(opts, target.WithWorkerFilter(item.GetWorkerFilter().GetValue()))
}
tcpAttrs := &pb.TcpTargetAttributes{}
if err := handlers.StructToProto(item.GetAttributes(), tcpAttrs); err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.")
}
if tcpAttrs.GetDefaultPort().GetValue() != 0 {
opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue()))
}
u, err := target.NewTcpTarget(item.GetScopeId(), opts...)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for creation: %v.", err)
}
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, m, err := repo.CreateTcpTarget(ctx, u)
if err != nil {
return nil, fmt.Errorf("unable to create target: %w", err)
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create target but no error returned from repository.")
}
return toProto(out, m)
}
func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Target) (*pb.Target, error) {
var opts []target.Option
if desc := item.GetDescription(); desc != nil {
opts = append(opts, target.WithDescription(desc.GetValue()))
}
if name := item.GetName(); name != nil {
opts = append(opts, target.WithName(name.GetValue()))
}
if item.GetSessionMaxSeconds() != nil {
opts = append(opts, target.WithSessionMaxSeconds(item.GetSessionMaxSeconds().GetValue()))
}
if item.GetSessionConnectionLimit() != nil {
opts = append(opts, target.WithSessionConnectionLimit(item.GetSessionConnectionLimit().GetValue()))
}
if filter := item.GetWorkerFilter(); filter != nil {
opts = append(opts, target.WithWorkerFilter(item.GetWorkerFilter().GetValue()))
}
tcpAttrs := &pb.TcpTargetAttributes{}
if err := handlers.StructToProto(item.GetAttributes(), tcpAttrs); err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.")
}
if tcpAttrs.GetDefaultPort().GetValue() != 0 {
opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue()))
}
version := item.GetVersion()
u, err := target.NewTcpTarget(scopeId, opts...)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for update: %v.", err)
}
u.PublicId = id
dbMask := maskManager.Translate(mask)
if len(dbMask) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid paths provided in the update mask."})
}
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, m, rowsUpdated, err := repo.UpdateTcpTarget(ctx, u, version, dbMask)
if err != nil {
return nil, fmt.Errorf("unable to update target: %w", err)
}
if rowsUpdated == 0 {
return nil, handlers.NotFoundErrorf("Target %q not found or incorrect version provided.", id)
}
return toProto(out, m)
}
func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
repo, err := s.repoFn()
if err != nil {
return false, err
}
rows, err := repo.DeleteTarget(ctx, id)
if err != nil {
if errors.IsNotFoundError(err) {
return false, nil
}
return false, fmt.Errorf("unable to delete target: %w", err)
}
return rows > 0, nil
}
func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
ul, err := repo.ListTargets(ctx, target.WithScopeIds(scopeIds))
if err != nil {
return nil, err
}
var outUl []*pb.Target
for _, u := range ul {
o, err := toProto(u, nil)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert value to proto: %v.", err)
}
outUl = append(outUl, o)
}
return outUl, nil
}
func (s Service) addInRepo(ctx context.Context, targetId string, hostSetId []string, version uint32) (*pb.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, m, err := repo.AddTargetHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetId, false))
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add host sets to target: %v.", err)
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding host sets to it.")
}
return toProto(out, m)
}
func (s Service) setInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (*pb.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
_, _, err = repo.SetTargetHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetIds, false))
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set host sets in target: %v.", err)
}
out, m, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, fmt.Errorf("unable to look up target after setting host sets: %w", err)
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after setting host sets for it.")
}
return toProto(out, m)
}
func (s Service) removeInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (*pb.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
_, err = repo.DeleteTargeHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetIds, false))
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove host sets from target: %v.", err)
}
out, m, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, fmt.Errorf("unable to look up target after removing host sets: %w", err)
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after removing host sets from it.")
}
return toProto(out, m)
}
func (s Service) authResult(ctx context.Context, id string, a action.Type, lookupOpt ...target.Option) auth.VerifyResults {
res := auth.VerifyResults{}
var parentId string
var t target.Target
opts := []auth.Option{auth.WithType(resource.Target), auth.WithAction(a)}
switch a {
case action.List, action.Create:
parentId = id
iamRepo, err := s.iamRepoFn()
if err != nil {
res.Error = err
return res
}
scp, err := iamRepo.LookupScope(ctx, parentId)
if err != nil {
res.Error = err
return res
}
if scp == nil {
res.Error = handlers.NotFoundError()
return res
}
default:
repo, err := s.repoFn()
if err != nil {
res.Error = err
return res
}
t, _, err = repo.LookupTarget(ctx, id, lookupOpt...)
if err != nil {
// TODO: Fix this with new/better error handling
if strings.Contains(err.Error(), "more than one row returned by a subquery") {
res.Error = handlers.ApiErrorWithCodeAndMessage(codes.FailedPrecondition, "Scope name is ambiguous (matches more than one scope), use scope ID with target name instead, or use target ID.")
} else {
res.Error = err
}
return res
}
if t == nil {
res.Error = handlers.NotFoundError()
return res
}
id = t.GetPublicId()
parentId = t.GetScopeId()
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
ret := auth.Verify(ctx, opts...)
ret.RoundTripValue = t
return ret
}
func toProto(in target.Target, m []*target.TargetSet) (*pb.Target, error) {
out := pb.Target{
Id: in.GetPublicId(),
ScopeId: in.GetScopeId(),
CreatedTime: in.GetCreateTime().GetTimestamp(),
UpdatedTime: in.GetUpdateTime().GetTimestamp(),
Version: in.GetVersion(),
Type: target.TcpTargetType.String(),
SessionMaxSeconds: wrapperspb.UInt32(in.GetSessionMaxSeconds()),
SessionConnectionLimit: wrapperspb.Int32(in.GetSessionConnectionLimit()),
}
if in.GetDescription() != "" {
out.Description = wrapperspb.String(in.GetDescription())
}
if in.GetName() != "" {
out.Name = wrapperspb.String(in.GetName())
}
if in.GetWorkerFilter() != "" {
out.WorkerFilter = wrapperspb.String(in.GetWorkerFilter())
}
attrs := &pb.TcpTargetAttributes{}
if in.GetDefaultPort() > 0 {
attrs.DefaultPort = &wrappers.UInt32Value{Value: in.GetDefaultPort()}
}
st, err := handlers.ProtoToStruct(attrs)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err)
}
out.Attributes = st
for _, hs := range m {
out.HostSetIds = append(out.HostSetIds, hs.GetPublicId())
out.HostSets = append(out.HostSets, &pb.HostSet{
Id: hs.GetPublicId(),
HostCatalogId: hs.GetCatalogId(),
})
}
return &out, nil
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// * The path passed in is correctly formatted
// * All required parameters are set
// * There are no conflicting parameters provided
func validateGetRequest(req *pbs.GetTargetRequest) error {
return handlers.ValidateGetRequest(target.TcpTargetPrefix, req, handlers.NoopValidatorFn)
}
func validateCreateRequest(req *pbs.CreateTargetRequest) error {
return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if !handlers.ValidId(scope.Project.Prefix(), req.GetItem().GetScopeId()) {
badFields["scope_id"] = "This field is required to have a properly formatted project scope id."
}
if req.GetItem().GetName() == nil || req.GetItem().GetName().GetValue() == "" {
badFields["name"] = "This field is required."
}
if req.GetItem().GetSessionConnectionLimit() != nil {
val := req.GetItem().GetSessionConnectionLimit().GetValue()
switch {
case val == -1:
case val > 0:
default:
badFields["session_connection_limit"] = "This must be -1 (unlimited) or greater than zero."
}
}
if req.GetItem().GetSessionMaxSeconds() != nil && req.GetItem().GetSessionMaxSeconds().GetValue() == 0 {
badFields["session_max_seconds"] = "This must be greater than zero."
}
switch target.SubtypeFromType(req.GetItem().GetType()) {
case target.TcpSubType:
tcpAttrs := &pb.TcpTargetAttributes{}
if err := handlers.StructToProto(req.GetItem().GetAttributes(), tcpAttrs); err != nil {
badFields["attributes"] = "Attribute fields do not match the expected format."
}
if tcpAttrs.GetDefaultPort() != nil && tcpAttrs.GetDefaultPort().GetValue() == 0 {
badFields["attributes.default_port"] = "This optional field cannot be set to 0."
}
}
switch req.GetItem().GetType() {
case target.TcpTargetType.String():
case "":
badFields["type"] = "This is a required field."
default:
badFields["type"] = "Unknown type provided."
}
if filter := req.GetItem().GetWorkerFilter(); filter != nil {
if _, err := bexpr.CreateEvaluator(filter.GetValue()); err != nil {
badFields["worker_filter"] = "Unable to successfully parse filter expression."
}
}
return badFields
})
}
func validateUpdateRequest(req *pbs.UpdateTargetRequest) error {
return handlers.ValidateUpdateRequest(target.TcpTargetPrefix, req, req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), "name") && req.GetItem().GetName().GetValue() == "" {
badFields["name"] = "This field cannot be set to empty."
}
if req.GetItem().GetSessionConnectionLimit() != nil {
val := req.GetItem().GetSessionConnectionLimit().GetValue()
switch {
case val == -1:
case val > 0:
default:
badFields["session_connection_limit"] = "This must be -1 (unlimited) or greater than zero."
}
}
if req.GetItem().GetSessionMaxSeconds() != nil && req.GetItem().GetSessionMaxSeconds().GetValue() == 0 {
badFields["session_max_seconds"] = "This must be greater than zero."
}
switch target.SubtypeFromId(req.GetItem().GetType()) {
case target.TcpSubType:
if req.GetItem().GetType() != "" && target.SubtypeFromType(req.GetItem().GetType()) != target.TcpSubType {
badFields["type"] = "Cannot modify the resource type."
}
tcpAttrs := &pb.TcpTargetAttributes{}
if err := handlers.StructToProto(req.GetItem().GetAttributes(), tcpAttrs); err != nil {
badFields["attributes"] = "Attribute fields do not match the expected format."
}
if tcpAttrs.GetDefaultPort() != nil && tcpAttrs.GetDefaultPort().GetValue() == 0 {
badFields["attributes.default_port"] = "This optional field cannot be set to 0."
}
}
if filter := req.GetItem().GetWorkerFilter(); filter != nil {
if _, err := bexpr.CreateEvaluator(filter.GetValue()); err != nil {
badFields["worker_filter"] = "Unable to successfully parse filter expression."
}
}
return badFields
})
}
func validateDeleteRequest(req *pbs.DeleteTargetRequest) error {
return handlers.ValidateDeleteRequest(target.TcpTargetPrefix, req, handlers.NoopValidatorFn)
}
func validateListRequest(req *pbs.ListTargetsRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) &&
!req.GetRecursive() {
badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
return nil
}
func validateAddRequest(req *pbs.AddTargetHostSetsRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) {
badFields["id"] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields["version"] = "Required field."
}
if len(req.GetHostSetIds()) == 0 {
badFields["host_set_ids"] = "Must be non-empty."
}
for _, id := range req.GetHostSetIds() {
if !handlers.ValidId(static.HostSetPrefix, id) {
badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateSetRequest(req *pbs.SetTargetHostSetsRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) {
badFields["id"] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields["version"] = "Required field."
}
for _, id := range req.GetHostSetIds() {
if !handlers.ValidId(static.HostSetPrefix, id) {
badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateRemoveRequest(req *pbs.RemoveTargetHostSetsRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) {
badFields["id"] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields["version"] = "Required field."
}
if len(req.GetHostSetIds()) == 0 {
badFields["host_set_ids"] = "Must be non-empty."
}
for _, id := range req.GetHostSetIds() {
if !handlers.ValidId(static.HostSetPrefix, id) {
badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateAuthorizeSessionRequest(req *pbs.AuthorizeSessionRequest) error {
badFields := map[string]string{}
nameEmpty := req.GetName() == ""
scopeIdEmpty := req.GetScopeId() == ""
scopeNameEmpty := req.GetScopeName() == ""
if nameEmpty {
if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) {
badFields["id"] = "Incorrectly formatted identifier."
}
if !scopeIdEmpty {
badFields["scope_id"] = "Scope ID provided when target name was empty."
}
if !scopeNameEmpty {
badFields["scope_id"] = "Scope name provided when target name was empty."
}
} else {
if req.GetName() != req.GetId() {
badFields["name"] = "Target name provided but does not match the given ID value from the URL."
}
switch {
case scopeIdEmpty && scopeNameEmpty:
badFields["scope_id"] = "Scope ID or scope name must be provided when target name is used."
badFields["scope_name"] = "Scope ID or scope name must be provided when target name is used."
case !scopeIdEmpty && !scopeNameEmpty:
badFields["scope_id"] = "Scope ID and scope name cannot both be provided when target name is used."
badFields["scope_name"] = "Scope ID and scope name cannot both be provided when target name is used."
}
}
if req.GetHostId() != "" {
switch host.SubtypeFromId(req.GetHostId()) {
case host.StaticSubtype:
default:
badFields["host_id"] = "Incorrectly formatted identifier."
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}