Files
threshold/internal/daemon/controller/handlers/targets/target_service.go
T
Johan Brandhorst-SatzkornandDamian Debkowski ec0a3f964b Worker status refactor (#5417)
* oss: feat(proto): statistics rpc added to server coordination service

* oss: feat(session): add update statistics status service

* oss: feat(handlers): add Statistics method to worker service server

* oss: feat(worker): add statistics ticking

* oss: internal/proto: define the new RoutingInfo RPC

* oss: internal/server: repository changes for RoutingInfo RPC

* oss: internal/daemon: add RoutingInfo handler to worker service

The new RoutingInfo RPC is used to get information from the worker
that is used to make routing decisions on the controller. It also
handles the initial request from the worker.

* oss: feat(db): server_worker_session_info table

* oss: feat(proto): session info rpc for server coordination service

* oss: feat(server): worker session info request data type

* oss: feat(worker): session info ticker

* oss: feat(handler): server coordination handler for session info

* oss: feat(session): utilize session info request time for session cleanup

* oss: proto: fix some gotags comments

Some of these fields were missing comments, while others had
incorrectly formatted comments.

* oss: worker: send connection status to server

This is required for the correct tracking of orphaned connections

* oss: internal/daemon/worker: add function to wait for Statistics RPC

* oss: internal/daemon/worker: add routing info RPC

The new RoutingInfo RPC replaces part of the Status RPC,
namely the part which is used by the controller and worker
to perform decisions around the establishment of new sessions
and new worker connections.

* oss: internal/daemon/worker: remove unnecessary error check

* oss: internal/daemon/worker: lock access to config and upstreams

The new confLock protects access to the conf value and to the
addressReceivers slice.

* oss: worker: replace Status RPC with RoutingInfo

* oss: worker: deprecate the Status RPC and related types

The Status RPC and related messages have been replaced
by the RoutingInfo/SessionInfo/Statistics RPCs. The controller
will continue to support the Status RPC until two new minor
versions have been released.

* oss: internal/event: add default deny for new worker RPCs

* oss: internal/daemon/worker: allow RPC interval to be set

The new SessionInfo/RoutingInfo/Statistics RPCs run at
long intervals, which make some of our cluster tests take
a long time to complete. This allows their interval to be
configured in tests.

* oss: all: use TestWorkerRPCInterval in slow tests

This should speed up these tests significantly.

* oss: internal/tests/cluster: reset sys eventer in test

---------

Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
2025-01-09 10:26:37 -08:00

2215 lines
79 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package targets
import (
"context"
stderrors "errors"
"fmt"
"math/rand"
"net"
"net/url"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/alias"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"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/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/event"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
intglobals "github.com/hashicorp/boundary/internal/globals"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/plugin"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/target"
"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/internal/types/subtypes"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
fm "github.com/hashicorp/boundary/version"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-secure-stdlib/strutil"
"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"
)
const (
credentialDomain = "credential"
hostDomain = "host"
)
// extraWorkerFilterFunc takes in a set of workers and returns another set,
// after any filtering it wishes to perform. When calling one of these
// functions, the current set should be passed in and the returned set should be
// used if there is no error; it is up to the filter writer to ensure that what
// is returned, if no filtering is desired, is the input set.
//
// This is generally used to take in a set selected already from the database
// and possible filtered via target worker filters and provide additional
// filtering capabilities on those remaining workers.
type extraWorkerFilterFunc func(ctx context.Context, workers []*server.Worker, host, port string) ([]*server.Worker, error)
var (
// ExtraWorkerFilters contains any custom worker filters that should be
// layered in at session authorization time. These will be executed in-order
// with the results from one fed into the next.
ExtraWorkerFilters []extraWorkerFilterFunc
// IdActions contains the set of actions that can be performed on
// individual resources
IdActions = action.NewActionSet(
action.NoOp,
action.Read,
action.Update,
action.Delete,
action.AddHostSources,
action.SetHostSources,
action.RemoveHostSources,
action.AddCredentialSources,
action.SetCredentialSources,
action.RemoveCredentialSources,
action.AuthorizeSession,
)
// CollectionActions contains the set of actions that can be performed on
// this collection
CollectionActions = action.NewActionSet(
action.Create,
action.List,
)
validateCredentialSourcesFn = func(context.Context, globals.Subtype, []target.CredentialSource) error { return nil }
ValidateIngressWorkerFilterFn = IngressWorkerFilterUnsupported
SessionRecordingFn = NoSessionRecording
WorkerFilterDeprecationMessage = fmt.Sprintf("This field is deprecated. Use %s instead.", globals.EgressWorkerFilterField)
StorageBucketFilterCredIdFn = noStorageBucket
)
func init() {
// TODO: refactor to remove IdActions and CollectionActions package variables
action.RegisterResource(resource.Target, IdActions, CollectionActions)
}
func IngressWorkerFilterUnsupported(string) error {
return fmt.Errorf("Ingress Worker Filter field is not supported in OSS")
}
// Service handles request as described by the pbs.TargetServiceServer interface.
type Service struct {
pbs.UnsafeTargetServiceServer
repoFn target.RepositoryFactory
aliasRepoFn common.TargetAliasRepoFactory
iamRepoFn common.IamRepoFactory
serversRepoFn common.ServersRepoFactory
sessionRepoFn session.RepositoryFactory
pluginHostRepoFn common.PluginHostRepoFactory
staticHostRepoFn common.StaticRepoFactory
vaultCredRepoFn common.VaultCredentialRepoFactory
staticCredRepoFn common.StaticCredentialRepoFactory
downstreams downstream.Graph
kmsCache *kms.Kms
workerRPCGracePeriod *atomic.Int64
maxPageSize uint
controllerExt intglobals.ControllerExtension
}
var _ pbs.TargetServiceServer = (*Service)(nil)
// NewService returns a target service which handles target related requests to boundary.
func NewService(
ctx context.Context,
kmsCache *kms.Kms,
repoFn target.RepositoryFactory,
iamRepoFn common.IamRepoFactory,
serversRepoFn common.ServersRepoFactory,
sessionRepoFn session.RepositoryFactory,
pluginHostRepoFn common.PluginHostRepoFactory,
staticHostRepoFn common.StaticRepoFactory,
vaultCredRepoFn common.VaultCredentialRepoFactory,
staticCredRepoFn common.StaticCredentialRepoFactory,
aliasRepoFn common.TargetAliasRepoFactory,
downstreams downstream.Graph,
workerRPCGracePeriod *atomic.Int64,
maxPageSize uint,
controllerExt intglobals.ControllerExtension,
) (Service, error) {
const op = "targets.NewService"
switch {
case kmsCache == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing kms repo")
case repoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing target repository")
case iamRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository")
case serversRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing server repository")
case sessionRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing session repository")
case pluginHostRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing plugin host repository")
case staticHostRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing static host repository")
case vaultCredRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing vault credential repository")
case staticCredRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing static credential repository")
case aliasRepoFn == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing target alias repository")
}
if maxPageSize == 0 {
maxPageSize = uint(globals.DefaultMaxPageSize)
}
return Service{
repoFn: repoFn,
iamRepoFn: iamRepoFn,
serversRepoFn: serversRepoFn,
sessionRepoFn: sessionRepoFn,
pluginHostRepoFn: pluginHostRepoFn,
staticHostRepoFn: staticHostRepoFn,
vaultCredRepoFn: vaultCredRepoFn,
staticCredRepoFn: staticCredRepoFn,
aliasRepoFn: aliasRepoFn,
downstreams: downstreams,
kmsCache: kmsCache,
workerRPCGracePeriod: workerRPCGracePeriod,
maxPageSize: maxPageSize,
controllerExt: controllerExt,
}, nil
}
// ListTargets implements the interface pbs.TargetServiceServer.
func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) (*pbs.ListTargetsResponse, error) {
const op = "targets.(Service).ListTargets"
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
// may have authorization on downstream scopes. Or, if they've not
// authenticated, still process in case u_anon has permissions.
if (authResults.Error == handlers.ForbiddenError() || authResults.Error == handlers.UnauthenticatedError()) &&
req.GetRecursive() &&
authResults.AuthenticationFinished {
} else {
return nil, authResults.Error
}
}
var err error
var authzScopes map[string]*scopes.ScopeInfo
if req.GetRecursive() {
authzScopes, err = authResults.ScopesAuthorizedForList(ctx, req.GetScopeId(), resource.Target)
} else {
authzScopes = map[string]*scopes.ScopeInfo{authResults.Scope.Id: authResults.Scope}
}
if err != nil {
return nil, err
}
// Get all user permissions for the requested scope(s).
userPerms := authResults.ACL().ListPermissions(authzScopes, resource.Target, IdActions, authResults.UserId)
if len(userPerms) == 0 {
return &pbs.ListTargetsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
}, nil
}
pageSize := int(s.maxPageSize)
// Use the requested page size only if it is smaller than
// the configured max.
if req.GetPageSize() != 0 && uint(req.GetPageSize()) < s.maxPageSize {
pageSize = int(req.GetPageSize())
}
var filterItemFn func(ctx context.Context, item target.Target) (bool, error)
switch {
case req.GetFilter() != "":
// Only use a filter if we need to
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
// TODO: replace the need for this function with some way to convert the `filter`
// to a domain type. This would allow filtering to happen in the domain, and we could
// remove this callback altogether.
filterItemFn = func(ctx context.Context, item target.Target) (bool, error) {
pbItem, err := toProto(ctx, item, newOutputOpts(ctx, item, authResults, authzScopes)...)
if err != nil {
return false, err
}
filterable, err := subtypes.Filterable(ctx, pbItem)
if err != nil {
return false, err
}
return filter.Match(filterable), nil
}
default:
filterItemFn = func(ctx context.Context, item target.Target) (bool, error) {
return true, nil
}
}
repo, err := s.repoFn(target.WithPermissions(userPerms))
if err != nil {
return nil, err
}
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, err
}
var listResp *pagination.ListResponse[target.Target]
var sortBy string
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = target.List(ctx, grantsHash, pageSize, filterItemFn, repo)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Target, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = target.ListPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = target.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = target.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
finalItems := make([]*pb.Target, 0, len(listResp.Items))
for _, item := range listResp.Items {
item, err := toProto(ctx, item, newOutputOpts(ctx, item, authResults, authzScopes)...)
if err != nil {
return nil, err
}
finalItems = append(finalItems, item)
}
respType := "delta"
if listResp.CompleteListing {
respType = "complete"
}
resp := &pbs.ListTargetsResponse{
Items: finalItems,
EstItemCount: uint32(listResp.EstimatedItemCount),
RemovedIds: listResp.DeletedIds,
ResponseType: respType,
SortBy: sortBy,
SortDir: "desc",
}
if listResp.ListToken != nil {
resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_TARGET)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetTarget implements the interface pbs.TargetServiceServer.
func (s Service) GetTarget(ctx context.Context, req *pbs.GetTargetRequest) (*pbs.GetTargetResponse, error) {
const op = "targets.(Service).GetTarget"
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
}
t, ts, cl, err := s.getFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.GetTargetResponse{Item: item}, nil
}
// CreateTarget implements the interface pbs.TargetServiceServer.
func (s Service) CreateTarget(ctx context.Context, req *pbs.CreateTargetRequest) (*pbs.CreateTargetResponse, error) {
const op = "targets.(Service).CreateTarget"
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
}
for _, a := range req.GetItem().WithAliases {
authResults := s.aliasCreateAuthResult(ctx, a.GetScopeId())
if authResults.Error != nil {
return nil, authResults.Error
}
}
t, ts, cl, err := s.createInRepo(ctx, req.GetItem())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.CreateTargetResponse{Item: item, Uri: fmt.Sprintf("targets/%s", item.GetId())}, nil
}
// UpdateTarget implements the interface pbs.TargetServiceServer.
func (s Service) UpdateTarget(ctx context.Context, req *pbs.UpdateTargetRequest) (*pbs.UpdateTargetResponse, error) {
const op = "targets.(Service).UpdateTarget"
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
}
t, ts, cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.UpdateTargetResponse{Item: item}, nil
}
// DeleteTarget implements the interface pbs.TargetServiceServer.
func (s Service) DeleteTarget(ctx context.Context, req *pbs.DeleteTargetRequest) (*pbs.DeleteTargetResponse, error) {
const op = "targets.(Service).DeleteTarget"
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 nil, nil
}
// AddTargetHostSources implements the interface pbs.TargetServiceServer.
func (s Service) AddTargetHostSources(ctx context.Context, req *pbs.AddTargetHostSourcesRequest) (*pbs.AddTargetHostSourcesResponse, error) {
const op = "targets.(Service).AddTargetHostSources"
if err := validateAddHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddHostSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, err := s.addHostSourcesInRepo(ctx, req.GetId(), req.GetHostSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
ts := t.GetHostSources()
cl := t.GetCredentialSources()
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.AddTargetHostSourcesResponse{Item: item}, nil
}
// SetTargetHostSources implements the interface pbs.TargetServiceServer.
func (s Service) SetTargetHostSources(ctx context.Context, req *pbs.SetTargetHostSourcesRequest) (*pbs.SetTargetHostSourcesResponse, error) {
const op = "targets.(Service).SetTargetHostSources"
if err := validateSetHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetHostSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, ts, cl, err := s.setHostSourcesInRepo(ctx, req.GetId(), req.GetHostSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.SetTargetHostSourcesResponse{Item: item}, nil
}
// RemoveTargetHostSources implements the interface pbs.TargetServiceServer.
func (s Service) RemoveTargetHostSources(ctx context.Context, req *pbs.RemoveTargetHostSourcesRequest) (*pbs.RemoveTargetHostSourcesResponse, error) {
const op = "targets.(Service).RemoveTargetHostSources"
if err := validateRemoveHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveHostSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, ts, cl, err := s.removeHostSourcesInRepo(ctx, req.GetId(), req.GetHostSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.RemoveTargetHostSourcesResponse{Item: item}, nil
}
// AddTargetCredentialSources implements the interface pbs.TargetServiceServer.
func (s Service) AddTargetCredentialSources(ctx context.Context, req *pbs.AddTargetCredentialSourcesRequest) (*pbs.AddTargetCredentialSourcesResponse, error) {
const op = "targets.(Service).AddTargetCredentialSources"
if err := validateAddCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddCredentialSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, ts, cl, err := s.addCredentialSourcesInRepo(ctx, req.GetId(), req.GetBrokeredCredentialSourceIds(), req.GetInjectedApplicationCredentialSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.AddTargetCredentialSourcesResponse{Item: item}, nil
}
// SetTargetCredentialSources implements the interface pbs.TargetServiceServer.
func (s Service) SetTargetCredentialSources(ctx context.Context, req *pbs.SetTargetCredentialSourcesRequest) (*pbs.SetTargetCredentialSourcesResponse, error) {
const op = "targets.(Service).SetTargetCredentialSources"
if err := validateSetCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetCredentialSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, ts, cl, err := s.setCredentialSourcesInRepo(ctx, req.GetId(), req.GetBrokeredCredentialSourceIds(), req.GetInjectedApplicationCredentialSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.SetTargetCredentialSourcesResponse{Item: item}, nil
}
// RemoveTargetCredentialSources implements the interface pbs.TargetServiceServer.
func (s Service) RemoveTargetCredentialSources(ctx context.Context, req *pbs.RemoveTargetCredentialSourcesRequest) (*pbs.RemoveTargetCredentialSourcesResponse, error) {
const op = "targets.(Service).RemoveTargetCredentialSources"
if err := validateRemoveCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveCredentialSources)
if authResults.Error != nil {
return nil, authResults.Error
}
t, ts, cl, err := s.removeCredentialSourcesInRepo(ctx, req.GetId(), req.GetBrokeredCredentialSourceIds(), req.GetInjectedApplicationCredentialSourceIds(), req.GetVersion())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings()))
}
t.SetHostSources(ts)
t.SetCredentialSources(cl)
item, err := toProto(ctx, t, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.RemoveTargetCredentialSourcesResponse{Item: item}, nil
}
func NoSessionRecording(context.Context, intglobals.ControllerExtension, *kms.Kms, target.Target, *session.Session, string) (string, error) {
return "", nil
}
func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSessionRequest) (_ *pbs.AuthorizeSessionResponse, retErr error) {
const op = "targets.(Service).AuthorizeSession"
if ctxAlias := alias.FromContext(ctx); ctxAlias != nil {
a, err := s.resolveAlias(ctx, ctxAlias.PublicId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if a.HostId != "" {
if req.GetHostId() != "" && req.GetHostId() != a.HostId {
return nil, handlers.InvalidArgumentErrorf("Errors in provided fields.", map[string]string{
"host_id": "The host id specified in the request does not match the one provided by the alias. Consider omitting the host id in the request.",
})
}
req.HostId = a.HostId
}
}
if err := validateAuthorizeSessionRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AuthorizeSession,
target.WithName(req.GetName()),
target.WithProjectId(req.GetScopeId()),
target.WithProjectName(req.GetScopeName()),
)
if authResults.Error != nil {
return nil, authResults.Error
}
correlationId, ok := event.CorrelationIdFromContext(ctx)
if !ok {
return nil, stderrors.New("authorize session: missing correlation id")
}
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()
}
if t.GetDefaultPort() == 0 {
return nil, handlers.ConflictErrorf("Target does not have default port defined.")
}
// Get the target information
repo, err := s.repoFn()
if err != nil {
return nil, err
}
t, 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())
}
hostSources := t.GetHostSources()
credSources := t.GetCredentialSources()
if len(credSources) > 0 {
if err := validateCredentialSourcesFn(ctx, t.GetType(), credSources); err != nil {
return nil, err
}
}
// Instantiate some repos
sessionRepo, err := s.sessionRepoFn()
if err != nil {
return nil, err
}
serversRepo, err := s.serversRepoFn()
if err != nil {
return nil, err
}
p := strconv.FormatUint(uint64(t.GetDefaultPort()), 10)
var h, hostId, hostSetId string
switch {
case t.GetAddress() != "":
h = t.GetAddress()
default:
requestedId := req.GetHostId()
staticHostRepo, err := s.staticHostRepoFn()
if err != nil {
return nil, err
}
pluginHostRepo, err := s.pluginHostRepoFn()
if err != nil {
return nil, err
}
var pluginHostSetIds []string
var endpoints []*host.Endpoint
for _, hSource := range hostSources {
hsId := hSource.Id()
switch globals.ResourceInfoFromPrefix(hsId).Subtype {
case static.Subtype:
eps, err := staticHostRepo.Endpoints(ctx, hsId)
if err != nil {
return nil, err
}
endpoints = append(endpoints, eps...)
default:
// Batch the plugin host set ids since each round trip to the plugin
// has the potential to be expensive.
pluginHostSetIds = append(pluginHostSetIds, hsId)
}
}
if len(pluginHostSetIds) > 0 {
eps, err := pluginHostRepo.Endpoints(ctx, pluginHostSetIds)
if err != nil {
return nil, err
}
endpoints = append(endpoints, eps...)
}
if len(endpoints) == 0 {
return nil, handlers.NotFoundErrorf("No host sources or address found for given target.")
}
var chosenEndpoint *host.Endpoint
if requestedId != "" {
for _, ep := range endpoints {
if ep.HostId == requestedId {
chosenEndpoint = ep
}
}
if chosenEndpoint == 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 chosenEndpoint == nil {
chosenEndpoint = endpoints[rand.Intn(len(endpoints))]
}
hostId = chosenEndpoint.HostId
hostSetId = chosenEndpoint.SetId
h = chosenEndpoint.Address
}
if h == "" {
return nil, handlers.ApiErrorWithCodeAndMessage(
codes.FailedPrecondition,
"No host was discovered after checking target address and host sources.")
}
// Ensure we don't have a port from the address
_, err = util.ParseAddress(ctx, h)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error when parsing the chosen endpoint host address"))
}
// Generate the endpoint URL
endpointUrl := &url.URL{
Scheme: t.GetType().String(),
Host: net.JoinHostPort(h, p),
}
// Get workers and filter down to ones that can service this request
selectedWorkers, protoWorkerId, err := serversRepo.SelectSessionWorkers(
ctx,
time.Duration(s.workerRPCGracePeriod.Load()),
t,
h,
s.controllerExt,
StorageBucketFilterCredIdFn,
s.downstreams,
)
if err != nil {
return nil, err
}
if len(selectedWorkers) == 0 {
return nil, handlers.ApiErrorWithCodeAndMessage(
codes.FailedPrecondition,
"No workers are available to handle this session.")
}
// Randomize the workers
rand.Shuffle(len(selectedWorkers), func(i, j int) {
selectedWorkers[i], selectedWorkers[j] = selectedWorkers[j], selectedWorkers[i]
})
var vaultReqs []credential.Request
var staticIds []string
var dynCreds []*session.DynamicCredential
var staticCreds []*session.StaticCredential
for _, cs := range credSources {
switch cs.Type() {
case target.LibraryCredentialSourceType:
vaultReqs = append(vaultReqs, credential.Request{
SourceId: cs.Id(),
Purpose: cs.CredentialPurpose(),
})
dynCreds = append(dynCreds, session.NewDynamicCredential(cs.Id(), cs.CredentialPurpose()))
case target.StaticCredentialSourceType:
staticIds = append(staticIds, cs.Id())
staticCreds = append(staticCreds, session.NewStaticCredential(cs.Id(), cs.CredentialPurpose()))
}
}
expTime := timestamppb.Now()
expTime.Seconds += int64(t.GetSessionMaxSeconds())
sessionComposition := session.ComposedOf{
UserId: authResults.UserId,
HostId: hostId,
TargetId: t.GetPublicId(),
HostSetId: hostSetId,
AuthTokenId: authResults.AuthTokenId,
ProjectId: authResults.Scope.Id,
Endpoint: endpointUrl.String(),
ExpirationTime: &timestamp.Timestamp{Timestamp: expTime},
ConnectionLimit: t.GetSessionConnectionLimit(),
WorkerFilter: t.GetWorkerFilter(),
EgressWorkerFilter: t.GetEgressWorkerFilter(),
IngressWorkerFilter: t.GetIngressWorkerFilter(),
DynamicCredentials: dynCreds,
StaticCredentials: staticCreds,
CorrelationId: correlationId,
ProtocolWorkerId: protoWorkerId,
}
sess, err := session.New(ctx, sessionComposition)
if err != nil {
return nil, err
}
wrapper, err := s.kmsCache.GetWrapper(ctx, authResults.Scope.Id, kms.KeyPurposeSessions)
if err != nil {
return nil, err
}
workerAddresses := make([]string, 0, len(selectedWorkers))
for _, sw := range selectedWorkers {
workerAddresses = append(workerAddresses, sw.Address)
}
sess, err = sessionRepo.CreateSession(ctx, wrapper, sess, workerAddresses)
if err != nil {
return nil, err
}
defer func() {
if retErr != nil {
// Delete created session in case of errors.
// Use new context for deletion in case error is because of context cancellation.
deleteCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := sessionRepo.DeleteSession(deleteCtx, sess.PublicId)
retErr = stderrors.Join(retErr, err)
}
}()
subtype := target.SubtypeFromId(t.GetPublicId())
subtypeEntry, err := subtypeRegistry.get(subtype)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if err := subtypeEntry.validateSessionStateFunc(ctx, sess); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var dynamic []credential.Dynamic
var staticCredsById map[string]credential.Static
if len(vaultReqs) > 0 {
credRepo, err := s.vaultCredRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
dynamic, err = credRepo.Issue(ctx, sess.GetPublicId(), vaultReqs, credential.WithTemplateData(authResults.UserData))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer func() {
if retErr != nil {
// Revoke issued credentials in case of errors.
// Use new context for deletion in case error is because of context cancellation.
deleteCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err := credRepo.Revoke(deleteCtx, sess.PublicId)
retErr = stderrors.Join(retErr, err)
// This leaves the credential in a state which will allow it to be cleaned up
// by the periodic credential cleanup job.
}
}()
}
if len(staticIds) > 0 {
credRepo, err := s.staticCredRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// Remove duplicate requests
staticIds = strutil.RemoveDuplicates(staticIds, false)
creds, err := credRepo.Retrieve(ctx, t.GetProjectId(), staticIds)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
staticCredsById = make(map[string]credential.Static)
for _, c := range creds {
staticCredsById[c.GetPublicId()] = c
}
}
var creds []*pb.SessionCredential
var workerCreds []session.Credential
for _, cred := range dynamic {
switch cred.Purpose() {
case credential.InjectedApplicationPurpose:
c, err := dynamicToWorkerCredential(ctx, cred)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
workerCreds = append(workerCreds, c)
case credential.BrokeredPurpose:
c, err := dynamicToSessionCredential(ctx, cred)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
creds = append(creds, c)
default:
return nil, errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unsupported credential purpose %s", cred.Purpose()))
}
}
for _, sc := range staticCreds {
switch sc.CredentialPurpose {
case string(credential.InjectedApplicationPurpose):
c, err := staticToWorkerCredential(ctx, staticCredsById[sc.CredentialStaticId])
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
workerCreds = append(workerCreds, c)
case string(credential.BrokeredPurpose):
c, err := staticToSessionCredential(ctx, staticCredsById[sc.CredentialStaticId])
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
creds = append(creds, c)
default:
return nil, errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unsupported credential purpose %s", sc.CredentialPurpose))
}
}
if len(workerCreds) > 0 {
// store credentials in repo, worker will request creds when a connection is established
// These credentials are deleted with the session, so nothing extra to cleanup in case of errors.
err = sessionRepo.AddSessionCredentials(ctx, sess.ProjectId, sess.PublicId, workerCreds)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
// this is an edge case issue where the hostId cannot be empty when trying to execute an ssh connection
// on a tcp target type. By setting the hostId to the targetId value, this will enable support of previous
// boundary cli versions.
if fm.SupportsFeature(fm.Binary, fm.UseTargetIdForHostId) && t.GetAddress() != "" {
hostId = t.GetPublicId()
}
workerInfos := make([]*pb.WorkerInfo, 0, len(selectedWorkers))
for _, sw := range selectedWorkers {
workerInfos = append(workerInfos, &pb.WorkerInfo{Address: sw.Address})
}
sad := &pb.SessionAuthorizationData{
SessionId: sess.PublicId,
TargetId: t.GetPublicId(),
Scope: authResults.Scope,
CreatedTime: sess.CreateTime.GetTimestamp(),
Expiration: sess.ExpirationTime.GetTimestamp(),
EndpointPort: t.GetDefaultPort(),
Type: t.GetType().String(),
Certificate: sess.Certificate,
PrivateKey: sess.CertificatePrivateKey,
HostId: hostId,
Endpoint: endpointUrl.String(),
WorkerInfo: workerInfos,
ConnectionLimit: t.GetSessionConnectionLimit(),
DefaultClientPort: t.GetDefaultClientPort(),
}
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(),
Expiration: sess.ExpirationTime.GetTimestamp(),
EndpointPort: t.GetDefaultPort(),
Type: t.GetType().String(),
AuthorizationToken: encodedMarshaledSad,
UserId: authResults.UserId,
HostId: hostId,
HostSetId: hostSetId,
Endpoint: endpointUrl.String(),
Credentials: creds,
ConnectionLimit: t.GetSessionConnectionLimit(),
}
ret.SessionRecordingId, err = SessionRecordingFn(
ctx,
s.controllerExt,
s.kmsCache,
t,
sess,
protoWorkerId,
)
if err != nil {
// Errors here will automatically delete the session and associated resources
// using deferred statements above.
return nil, errors.Wrap(ctx, err, op)
}
return &pbs.AuthorizeSessionResponse{Item: ret}, nil
}
func (s Service) getFromRepo(ctx context.Context, id string) (target.Target, []target.HostSource, []target.CredentialSource, error) {
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
u, err := repo.LookupTarget(ctx, id)
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil, nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id)
}
return nil, nil, nil, err
}
hs := u.GetHostSources()
cl := u.GetCredentialSources()
if u == nil {
return nil, nil, nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id)
}
return u, hs, cl, nil
}
// resolveAlias returns the alias resource with the specified public id.
func (s Service) resolveAlias(ctx context.Context, id string) (*talias.Alias, error) {
const op = "targets.(Service).resolveAlias"
if id == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "alias id is empty")
}
r, err := s.aliasRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
a, err := r.LookupAlias(ctx, id)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if a == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.NotFound, "alias with id %q not found", id)
}
return a, nil
}
func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).createInRepo"
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.GetEgressWorkerFilter() != nil {
opts = append(opts, target.WithEgressWorkerFilter(item.GetEgressWorkerFilter().GetValue()))
}
if item.GetIngressWorkerFilter() != nil {
opts = append(opts, target.WithIngressWorkerFilter(item.GetIngressWorkerFilter().GetValue()))
}
if item.GetAddress() != nil {
opts = append(opts, target.WithAddress(strings.TrimSpace(item.GetAddress().GetValue())))
}
attr, err := subtypeRegistry.newAttribute(target.SubtypeFromType(item.GetType()), item.GetAttrs())
if err != nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error())
}
opts = append(opts, attr.Options()...)
u, err := target.New(ctx, target.SubtypeFromType(item.GetType()), item.GetScopeId(), opts...)
if err != nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for creation: %v.", err)
}
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
createOptions := []target.Option{}
if len(item.GetWithAliases()) > 0 {
writeAliases := make([]*talias.Alias, 0, len(item.GetWithAliases()))
for _, a := range item.GetWithAliases() {
na, err := talias.NewAlias(ctx, a.GetScopeId(), a.GetValue(), talias.WithHostId(a.GetAttributes().GetAuthorizeSessionArguments().GetHostId()))
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("converting from proto to storage alias"))
}
writeAliases = append(writeAliases, na)
}
createOptions = append(createOptions, target.WithAliases(writeAliases))
}
out, err := repo.CreateTarget(ctx, u, createOptions...)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target"))
}
hs := out.GetHostSources()
cl := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create target but no error returned from repository.")
}
return out, hs, cl, nil
}
func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Target) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).updateInRepo"
var dbMask []string
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()))
}
// worker_filter is deprecated, but we allow users who have migrated with a worker_filter value to update it.
if workerFilter := item.GetWorkerFilter(); workerFilter != nil {
opts = append(opts, target.WithWorkerFilter(item.GetWorkerFilter().GetValue()))
}
if egressFilter := item.GetEgressWorkerFilter(); egressFilter != nil {
opts = append(opts, target.WithEgressWorkerFilter(item.GetEgressWorkerFilter().GetValue()))
}
if ingressFilter := item.GetIngressWorkerFilter(); ingressFilter != nil {
opts = append(opts, target.WithIngressWorkerFilter(item.GetIngressWorkerFilter().GetValue()))
}
if item.GetAddress() != nil {
dbMask = append(dbMask, "Address")
opts = append(opts, target.WithAddress(strings.TrimSpace(item.GetAddress().GetValue())))
}
subtype := target.SubtypeFromId(id)
attr, err := subtypeRegistry.newAttribute(subtype, item.GetAttrs())
if err != nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error())
}
opts = append(opts, attr.Options()...)
version := item.GetVersion()
u, err := target.New(ctx, subtype, scopeId, opts...)
if err != nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for update: %v.", err)
}
if err := u.SetPublicId(ctx, id); err != nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set target id: %v.", err)
}
maskManager, err := subtypeRegistry.maskManager(subtype)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update target"))
}
dbMask = append(dbMask, maskManager.Translate(mask)...)
if len(dbMask) == 0 {
return nil, nil, 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, nil, nil, err
}
out, rowsUpdated, err := repo.UpdateTarget(ctx, u, version, dbMask, opts...)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update target"))
}
hs := out.GetHostSources()
cl := out.GetCredentialSources()
if rowsUpdated == 0 {
return nil, nil, nil, handlers.NotFoundErrorf("Target %q not found or incorrect version provided.", id)
}
return out, hs, cl, nil
}
func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
const op = "targets.(Service).deleteFromRepo"
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, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete target"))
}
return rows > 0, nil
}
func (s Service) addHostSourcesInRepo(ctx context.Context, targetId string, hostSourceIds []string, version uint32) (target.Target, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, err := repo.AddTargetHostSources(ctx, targetId, version, strutil.RemoveDuplicates(hostSourceIds, false))
if err != nil {
var internalErr *errors.Err
if stderrors.As(err, &internalErr) && internalErr.Code == errors.Conflict {
// The conflict error is surfaced directly as it's correctly
// converted all the way down to the HTTP status.
return nil, err
}
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Failed to add target host sources")
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding host sources to it.")
}
return out, nil
}
func (s Service) setHostSourcesInRepo(ctx context.Context, targetId string, hostSourceIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).setSourcesInRepo"
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
_, _, _, err = repo.SetTargetHostSources(ctx, targetId, version, strutil.RemoveDuplicates(hostSourceIds, false))
if err != nil {
var internalErr *errors.Err
if stderrors.As(err, &internalErr) && internalErr.Code == errors.Conflict {
// The conflict error is surfaced directly as it's correctly
// converted all the way down to the HTTP status.
return nil, nil, nil, err
}
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Failed to set target host sources")
}
out, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to look up target after setting host sources"))
}
hs := out.GetHostSources()
cl := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after setting host sources for it.")
}
return out, hs, cl, nil
}
func (s Service) removeHostSourcesInRepo(ctx context.Context, targetId string, hostSourceIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).removeHostSourcesInRepo"
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
_, err = repo.DeleteTargetHostSources(ctx, targetId, version, strutil.RemoveDuplicates(hostSourceIds, false))
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove host sources from target: %v.", err)
}
out, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to look up target after removing host sources"))
}
hs := out.GetHostSources()
cl := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after removing host sources from it.")
}
return out, hs, cl, nil
}
func (s Service) addCredentialSourcesInRepo(ctx context.Context, targetId string, brokeredIds []string, injectedAppIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) {
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
var creds target.CredentialSources
if len(brokeredIds) > 0 {
creds.BrokeredCredentialIds = strutil.RemoveDuplicates(brokeredIds, false)
}
if len(injectedAppIds) > 0 {
creds.InjectedApplicationCredentialIds = strutil.RemoveDuplicates(injectedAppIds, false)
}
out, err := repo.AddTargetCredentialSources(ctx, targetId, version, creds)
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add credential sources to target: %v.", err)
}
hs := out.GetHostSources()
credSources := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding credential sources to it.")
}
return out, hs, credSources, nil
}
func (s Service) setCredentialSourcesInRepo(ctx context.Context, targetId string, brokeredIds []string, injectedAppIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).setCredentialSourcesInRepo"
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
var ids target.CredentialSources
if len(brokeredIds) > 0 {
ids.BrokeredCredentialIds = strutil.RemoveDuplicates(brokeredIds, false)
}
if len(injectedAppIds) > 0 {
ids.InjectedApplicationCredentialIds = strutil.RemoveDuplicates(injectedAppIds, false)
}
_, _, _, err = repo.SetTargetCredentialSources(ctx, targetId, version, ids)
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set credential sources in target: %v.", err)
}
out, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to look up target after setting credential sources"))
}
hs := out.GetHostSources()
credSources := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after setting credential sources for it.")
}
return out, hs, credSources, nil
}
func (s Service) removeCredentialSourcesInRepo(ctx context.Context, targetId string, brokeredIds []string, injectedAppIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) {
const op = "targets.(Service).removeCredentialSourcesInRepo"
repo, err := s.repoFn()
if err != nil {
return nil, nil, nil, err
}
var ids target.CredentialSources
if len(brokeredIds) > 0 {
ids.BrokeredCredentialIds = strutil.RemoveDuplicates(brokeredIds, false)
}
if len(injectedAppIds) > 0 {
ids.InjectedApplicationCredentialIds = strutil.RemoveDuplicates(injectedAppIds, false)
}
_, err = repo.DeleteTargetCredentialSources(ctx, targetId, version, ids)
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove credential sources from target: %v.", err)
}
out, err := repo.LookupTarget(ctx, targetId)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to look up target after removing credential sources"))
}
hs := out.GetHostSources()
credSources := out.GetCredentialSources()
if out == nil {
return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after removing credential sources from it.")
}
return out, hs, credSources, nil
}
// aliasCreateAuthResult verifies authorization for creating an alias
func (s Service) aliasCreateAuthResult(ctx context.Context, parentId string) auth.VerifyResults {
res := auth.VerifyResults{}
a := action.Create
opts := []auth.Option{auth.WithType(resource.Alias), auth.WithAction(a)}
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
}
opts = append(opts, auth.WithScopeId(parentId))
ret := auth.Verify(ctx, opts...)
return ret
}
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.GetProjectId()
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
ret := auth.Verify(ctx, opts...)
ret.RoundTripValue = t
return ret
}
func toProto(ctx context.Context, in target.Target, opt ...handlers.Option) (*pb.Target, error) {
const op = "target_service.toProto"
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building target proto")
}
outputFields := *opts.WithOutputFields
hostSources := in.GetHostSources()
credSources := in.GetCredentialSources()
aliases := in.GetAliases()
out := pb.Target{}
if outputFields.Has(globals.IdField) {
out.Id = in.GetPublicId()
}
if outputFields.Has(globals.ScopeIdField) {
out.ScopeId = in.GetProjectId()
}
if outputFields.Has(globals.TypeField) {
out.Type = in.GetType().String()
}
if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" {
out.Description = wrapperspb.String(in.GetDescription())
}
if outputFields.Has(globals.NameField) && in.GetName() != "" {
out.Name = wrapperspb.String(in.GetName())
}
if outputFields.Has(globals.CreatedTimeField) {
out.CreatedTime = in.GetCreateTime().GetTimestamp()
}
if outputFields.Has(globals.UpdatedTimeField) {
out.UpdatedTime = in.GetUpdateTime().GetTimestamp()
}
if outputFields.Has(globals.VersionField) {
out.Version = in.GetVersion()
}
if outputFields.Has(globals.SessionMaxSecondsField) {
out.SessionMaxSeconds = wrapperspb.UInt32(in.GetSessionMaxSeconds())
}
if outputFields.Has(globals.SessionConnectionLimitField) {
out.SessionConnectionLimit = wrapperspb.Int32(in.GetSessionConnectionLimit())
}
if outputFields.Has(globals.WorkerFilterField) && in.GetWorkerFilter() != "" {
out.WorkerFilter = wrapperspb.String(in.GetWorkerFilter())
}
if outputFields.Has(globals.EgressWorkerFilterField) && in.GetEgressWorkerFilter() != "" {
out.EgressWorkerFilter = wrapperspb.String(in.GetEgressWorkerFilter())
}
if outputFields.Has(globals.IngressWorkerFilterField) && in.GetIngressWorkerFilter() != "" {
out.IngressWorkerFilter = wrapperspb.String(in.GetIngressWorkerFilter())
}
if outputFields.Has(globals.ScopeField) {
out.Scope = opts.WithScope
}
if outputFields.Has(globals.AuthorizedActionsField) {
out.AuthorizedActions = opts.WithAuthorizedActions
}
if outputFields.Has(globals.HostSourceIdsField) {
for _, hs := range hostSources {
out.HostSourceIds = append(out.HostSourceIds, hs.Id())
}
}
if outputFields.Has(globals.HostSourcesField) {
for _, hs := range hostSources {
out.HostSources = append(out.HostSources, &pb.HostSource{
Id: hs.Id(),
HostCatalogId: hs.HostCatalogId(),
})
}
}
if outputFields.Has(globals.AliasesField) {
for _, a := range aliases {
// Even though pb.Alias has more than just these 2 fields, we only
// want to return these 2 fields to the client. Any more information
// may be sharing more than the client should know.
out.Aliases = append(out.Aliases, &pb.Alias{
Id: a.PublicId,
Value: a.Value,
})
}
}
if outputFields.Has(globals.AddressField) {
out.Address = wrapperspb.String(in.GetAddress())
}
var brokeredSources, injectedAppSources []*pb.CredentialSource
var brokeredSourceIds, injectedAppSourceIds []string
for _, cs := range credSources {
switch cs.CredentialPurpose() {
case credential.BrokeredPurpose:
brokeredSourceIds = append(brokeredSourceIds, cs.Id())
brokeredSources = append(brokeredSources, &pb.CredentialSource{
Id: cs.Id(),
CredentialStoreId: cs.CredentialStoreId(),
})
case credential.InjectedApplicationPurpose:
injectedAppSources = append(injectedAppSources, &pb.CredentialSource{
Id: cs.Id(),
CredentialStoreId: cs.CredentialStoreId(),
})
injectedAppSourceIds = append(injectedAppSourceIds, cs.Id())
default:
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unrecognized purpose %q for credential source on target", cs.CredentialPurpose()))
}
}
if outputFields.Has(globals.BrokeredCredentialSourceIdsField) {
out.BrokeredCredentialSourceIds = brokeredSourceIds
}
if outputFields.Has(globals.BrokeredCredentialSourcesField) {
out.BrokeredCredentialSources = brokeredSources
}
if outputFields.Has(globals.InjectedApplicationCredentialSourceIdsField) {
out.InjectedApplicationCredentialSourceIds = injectedAppSourceIds
}
if outputFields.Has(globals.InjectedApplicationCredentialSourcesField) {
out.InjectedApplicationCredentialSources = injectedAppSources
}
if outputFields.Has(globals.AttributesField) {
if err := subtypeRegistry.setAttributes(in.GetType(), in, &out); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
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(handlers.NoopValidatorFn, req, target.Prefixes()...)
}
func validateCreateRequest(req *pbs.CreateTargetRequest) error {
item := req.GetItem()
return handlers.ValidateCreateRequest(item, func() map[string]string {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Project.Prefix()) {
badFields[globals.ScopeIdField] = "This field is required to have a properly formatted project scope id."
}
if item.GetName() == nil || item.GetName().GetValue() == "" {
badFields[globals.NameField] = "This field is required."
}
for _, a := range item.GetWithAliases() {
pa := proto.Clone(a).(*pb.Alias)
pa.Value = ""
pa.ScopeId = ""
pa.Attributes = nil
if !proto.Equal(pa, &pb.Alias{}) {
badFields[globals.WithAliasesField] = "Included aliases can only specify a value, a scope id, and attributes."
}
}
if item.GetSessionConnectionLimit() != nil {
val := item.GetSessionConnectionLimit().GetValue()
switch {
case val == -1:
case val > 0:
default:
badFields[globals.SessionConnectionLimitField] = "This must be -1 (unlimited) or greater than zero."
}
}
if item.GetSessionMaxSeconds() != nil && item.GetSessionMaxSeconds().GetValue() == 0 {
badFields[globals.SessionMaxSecondsField] = "This must be greater than zero."
}
if item.GetType() == "" {
badFields[globals.TypeField] = "This is a required field."
} else if target.SubtypeFromType(item.GetType()) == "" {
badFields[globals.TypeField] = "Unknown type provided."
}
if workerFilter := item.GetWorkerFilter(); workerFilter != nil {
badFields[globals.WorkerFilterField] = WorkerFilterDeprecationMessage
}
if egressFilter := item.GetEgressWorkerFilter(); egressFilter != nil {
if _, err := bexpr.CreateEvaluator(egressFilter.GetValue()); err != nil {
badFields[globals.EgressWorkerFilterField] = "Unable to successfully parse egress filter expression."
}
}
if ingressFilter := item.GetIngressWorkerFilter(); ingressFilter != nil {
err := ValidateIngressWorkerFilterFn(ingressFilter.GetValue())
if err != nil {
badFields[globals.IngressWorkerFilterField] = err.Error()
}
}
if address := item.GetAddress(); address != nil {
if len(address.GetValue()) < static.MinHostAddressLength ||
len(address.GetValue()) > static.MaxHostAddressLength {
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
}
_, _, err := net.SplitHostPort(address.GetValue())
switch {
case err == nil:
badFields[globals.AddressField] = "Address does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
default:
badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
}
subtype := target.SubtypeFromType(item.GetType())
_, err := subtypeRegistry.get(subtype)
if err != nil {
badFields[globals.TypeField] = "Unknown type provided."
} else {
a, err := subtypeRegistry.newAttribute(subtype, item.GetAttrs())
if err != nil {
badFields[globals.AttributesField] = "Attribute fields do not match the expected format."
} else {
for k, v := range a.Vet() {
badFields[k] = v
}
}
}
return badFields
})
}
func validateUpdateRequest(req *pbs.UpdateTargetRequest) error {
item := req.GetItem()
return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string {
badFields := map[string]string{}
paths := req.GetUpdateMask().GetPaths()
if handlers.MaskContains(paths, globals.NameField) && item.GetName().GetValue() == "" {
badFields[globals.NameField] = "This field cannot be set to empty."
}
if item.GetSessionConnectionLimit() != nil {
val := item.GetSessionConnectionLimit().GetValue()
switch {
case val == -1:
case val > 0:
default:
badFields[globals.SessionConnectionLimitField] = "This must be -1 (unlimited) or greater than zero."
}
}
if item.GetSessionMaxSeconds() != nil && item.GetSessionMaxSeconds().GetValue() == 0 {
badFields[globals.SessionMaxSecondsField] = "This must be greater than zero."
}
if len(item.GetWithAliases()) > 0 {
badFields[globals.WithAliasesField] = "This field can only be set at target creation time."
}
// worker_filter is mutually exclusive from ingress and egress filter
workerFilterFound := false
if workerFilter := item.GetWorkerFilter(); workerFilter != nil {
if _, err := bexpr.CreateEvaluator(workerFilter.GetValue()); err != nil {
badFields[globals.WorkerFilterField] = "Unable to successfully parse filter expression."
}
workerFilterFound = true
}
if egressFilter := item.GetEgressWorkerFilter(); egressFilter != nil {
if workerFilterFound {
badFields[globals.EgressWorkerFilterField] = fmt.Sprintf("Cannot set %s and %s; they are mutually exclusive fields.", globals.WorkerFilterField, globals.EgressWorkerFilterField)
}
if _, err := bexpr.CreateEvaluator(egressFilter.GetValue()); err != nil {
badFields[globals.EgressWorkerFilterField] = "Unable to successfully parse egress filter expression."
}
}
if ingressFilter := item.GetIngressWorkerFilter(); ingressFilter != nil {
if workerFilterFound {
badFields[globals.IngressWorkerFilterField] = fmt.Sprintf("Cannot set %s and %s; they are mutually exclusive fields.", globals.WorkerFilterField, globals.IngressWorkerFilterField)
}
err := ValidateIngressWorkerFilterFn(ingressFilter.GetValue())
if err != nil {
badFields[globals.IngressWorkerFilterField] = err.Error()
}
}
if address := item.GetAddress(); address != nil {
if len(address.GetValue()) < static.MinHostAddressLength ||
len(address.GetValue()) > static.MaxHostAddressLength {
badFields[globals.AddressField] = fmt.Sprintf("Address length must be between %d and %d characters.", static.MinHostAddressLength, static.MaxHostAddressLength)
}
_, _, err := net.SplitHostPort(address.GetValue())
switch {
case err == nil:
badFields[globals.AddressField] = "Address does not support a port."
case strings.Contains(err.Error(), globals.MissingPortErrStr):
default:
badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err)
}
}
subtype := target.SubtypeFromId(req.GetId())
_, err := subtypeRegistry.get(subtype)
if err != nil {
badFields[globals.TypeField] = "Unknown type provided."
} else {
if item.GetType() != "" && target.SubtypeFromType(item.GetType()) != subtype {
badFields[globals.TypeField] = "Cannot modify the resource type."
}
a, err := subtypeRegistry.newAttribute(subtype, item.GetAttrs())
if err != nil {
badFields[globals.AttributesField] = "Attribute fields do not match the expected format."
} else {
for k, v := range a.VetForUpdate(paths) {
badFields[k] = v
}
}
}
return badFields
}, target.Prefixes()...)
}
func validateDeleteRequest(req *pbs.DeleteTargetRequest) error {
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, target.Prefixes()...)
}
func validateListRequest(ctx context.Context, req *pbs.ListTargetsRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) &&
!req.GetRecursive() {
badFields[globals.ScopeIdField] = "This field must be a valid project scope ID or the list operation must be recursive."
}
if _, err := handlers.NewFilter(ctx, req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
return nil
}
func newOutputOpts(ctx context.Context, item target.Target, authResults auth.VerifyResults, authzScopes map[string]*scopes.ScopeInfo) []handlers.Option {
pr := perms.Resource{Id: item.GetPublicId(), ScopeId: item.GetProjectId(), Type: resource.Target, ParentScopeId: authzScopes[item.GetProjectId()].GetParentScopeId()}
outputFields := authResults.FetchOutputFields(pr, action.List).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authzScopes[item.GetProjectId()]))
}
if outputFields.Has(globals.AuthorizedActionsField) {
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&pr)).Strings()
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
return outputOpts
}
func validateAddHostSourcesRequest(req *pbs.AddTargetHostSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
if len(req.GetHostSourceIds()) == 0 {
badFields[globals.HostSourceIdsField] = "Must be non-empty."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), globals.StaticHostSetPrefix, globals.PluginHostSetPrefix, globals.PluginHostSetPreviousPrefix) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateSetHostSourcesRequest(req *pbs.SetTargetHostSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), globals.StaticHostSetPrefix, globals.PluginHostSetPrefix, globals.PluginHostSetPreviousPrefix) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateRemoveHostSourcesRequest(req *pbs.RemoveTargetHostSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
if len(req.GetHostSourceIds()) == 0 {
badFields[globals.HostSourceIdsField] = "Must be non-empty."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), globals.StaticHostSetPrefix, globals.PluginHostSetPrefix, globals.PluginHostSetPreviousPrefix) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
if len(req.GetBrokeredCredentialSourceIds())+len(req.GetInjectedApplicationCredentialSourceIds()) == 0 {
badFields[globals.BrokeredCredentialSourceIdsField] = "Brokered or Injected Application Credential Source IDs must be provided."
badFields[globals.InjectedApplicationCredentialSourceIdsField] = "Brokered or Injected Application Credential Source IDs must be provided."
}
for _, cl := range req.GetBrokeredCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix,
globals.JsonCredentialPrefix) {
badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetInjectedApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.VaultSshCertificateCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix) {
badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
for _, cl := range req.GetBrokeredCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix,
globals.JsonCredentialPrefix) {
badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetInjectedApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.VaultSshCertificateCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix) {
badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourcesRequest) error {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if req.GetVersion() == 0 {
badFields[globals.VersionField] = "Required field."
}
if len(req.GetBrokeredCredentialSourceIds())+len(req.GetInjectedApplicationCredentialSourceIds()) == 0 {
badFields[globals.BrokeredCredentialSourceIdsField] = "Brokered or Injected Application Credential Source IDs must be provided."
badFields[globals.InjectedApplicationCredentialSourceIdsField] = "Brokered or Injected Application Credential Source IDs must be provided."
}
for _, cl := range req.GetBrokeredCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix,
globals.JsonCredentialPrefix) {
badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetInjectedApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl),
globals.VaultCredentialLibraryPrefix,
globals.VaultSshCertificateCredentialLibraryPrefix,
globals.UsernamePasswordCredentialPrefix,
globals.UsernamePasswordCredentialPreviousPrefix,
globals.SshPrivateKeyCredentialPrefix,
globals.JsonCredentialPrefix) {
badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
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(handlers.Id(req.GetId()), target.Prefixes()...) {
badFields[globals.IdField] = "Incorrectly formatted identifier."
}
if !scopeIdEmpty {
badFields[globals.ScopeIdField] = "Scope ID provided when target name was empty."
}
if !scopeNameEmpty {
badFields[globals.ScopeIdField] = "Scope name provided when target name was empty."
}
} else {
if req.GetName() != req.GetId() {
badFields[globals.NameField] = "Target name provided but does not match the given ID value from the URL."
}
switch {
case scopeIdEmpty && scopeNameEmpty:
badFields[globals.ScopeIdField] = "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[globals.ScopeIdField] = "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 globals.ResourceInfoFromPrefix(req.GetHostId()).Subtype {
case static.Subtype, plugin.Subtype:
default:
badFields[globals.HostIdField] = "Incorrectly formatted identifier."
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
}
return nil
}
func noStorageBucket(_ context.Context, _ intglobals.ControllerExtension, _ string) (string, string, error) {
return "", "", fmt.Errorf("not supported")
}