From 715982148f8b2fa57a535f115debc822010bf98d Mon Sep 17 00:00:00 2001 From: Todd Knight Date: Mon, 13 Sep 2021 15:02:04 -0700 Subject: [PATCH] Create Plugin Host Set Handler for Create, List, Get (#1518) --- internal/servers/controller/common/common.go | 2 + internal/servers/controller/controller.go | 5 + internal/servers/controller/handler.go | 2 +- .../handlers/host_sets/host_set_service.go | 388 ++++++++++++---- .../host_sets/host_set_service_test.go | 439 +++++++++++++++++- 5 files changed, 734 insertions(+), 102 deletions(-) diff --git a/internal/servers/controller/common/common.go b/internal/servers/controller/common/common.go index 1c07bfa68..dfa1405ca 100644 --- a/internal/servers/controller/common/common.go +++ b/internal/servers/controller/common/common.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/credential/vault" + pluginhost "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers" @@ -19,6 +20,7 @@ type ( PasswordAuthRepoFactory func() (*password.Repository, error) ServersRepoFactory func() (*servers.Repository, error) StaticRepoFactory func() (*static.Repository, error) + PluginHostRepoFactory func() (*pluginhost.Repository, error) SessionRepoFactory func() (*session.Repository, error) TargetRepoFactory func() (*target.Repository, error) ) diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 0e389e3b2..bf04b5eb4 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" @@ -53,6 +54,7 @@ type Controller struct { ServersRepoFn common.ServersRepoFactory SessionRepoFn common.SessionRepoFactory StaticHostRepoFn common.StaticRepoFactory + PluginHostRepoFn common.PluginHostRepoFactory TargetRepoFn common.TargetRepoFactory scheduler *scheduler.Scheduler @@ -131,6 +133,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { c.StaticHostRepoFn = func() (*static.Repository, error) { return static.NewRepository(dbase, dbase, c.kms) } + c.PluginHostRepoFn = func() (*plugin.Repository, error) { + return plugin.NewRepository(dbase, dbase, c.kms) + } c.AuthTokenRepoFn = func() (*authtoken.Repository, error) { return authtoken.NewRepository(dbase, dbase, c.kms, authtoken.WithTokenTimeToLiveDuration(c.conf.RawConfig.Controller.AuthTokenTimeToLiveDuration), diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 0bd972914..e4dd0dbec 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -92,7 +92,7 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil { return nil, fmt.Errorf("failed to register host catalog service handler: %w", err) } - hss, err := host_sets.NewService(c.StaticHostRepoFn) + hss, err := host_sets.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn) if err != nil { return nil, fmt.Errorf("failed to create host set handler service: %w", err) } diff --git a/internal/servers/controller/handlers/host_sets/host_set_service.go b/internal/servers/controller/handlers/host_sets/host_set_service.go index a251efaca..84965f865 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service.go @@ -2,12 +2,15 @@ package host_sets import ( "context" + "encoding/json" "fmt" + "strings" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "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/host/static/store" "github.com/hashicorp/boundary/internal/perms" @@ -20,6 +23,7 @@ import ( pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" "github.com/hashicorp/go-secure-stdlib/strutil" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -57,18 +61,22 @@ type Service struct { pbs.UnimplementedHostSetServiceServer staticRepoFn common.StaticRepoFactory + pluginRepoFn common.PluginHostRepoFactory } var _ pbs.HostSetServiceServer = Service{} // NewService returns a host set Service which handles host set related requests to boundary and uses the provided // repositories for storage and retrieval. -func NewService(repoFn common.StaticRepoFactory) (Service, error) { +func NewService(staticRepoFn common.StaticRepoFactory, pluginRepoFn common.PluginHostRepoFactory) (Service, error) { const op = "host_sets.NewService" - if repoFn == nil { + if staticRepoFn == nil { return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing static repository") } - return Service{staticRepoFn: repoFn}, nil + if pluginRepoFn == nil { + return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing plugin repository") + } + return Service{staticRepoFn: staticRepoFn, pluginRepoFn: pluginRepoFn}, nil } func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest) (*pbs.ListHostSetsResponse, error) { @@ -217,7 +225,7 @@ func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetReques if authResults.Error != nil { return nil, authResults.Error } - hs, hosts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + hs, hosts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req) if err != nil { return nil, err } @@ -373,63 +381,96 @@ func (s Service) RemoveHostSetHosts(ctx context.Context, req *pbs.RemoveHostSetH return &pbs.RemoveHostSetHostsResponse{Item: item}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*static.HostSet, []*static.Host, error) { - repo, err := s.staticRepoFn() - if err != nil { - return nil, nil, err +func (s Service) getFromRepo(ctx context.Context, id string) (host.Set, []host.Host, error) { + var hs host.Set + var hl []host.Host + switch host.SubtypeFromId(id) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return nil, nil, err + } + hset, hosts, err := repo.LookupSet(ctx, id) + if err != nil { + return nil, nil, err + } + if hset == nil { + return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id) + } + for _, h := range hosts { + hl = append(hl, h) + } + hs = hset + case plugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return nil, nil, err + } + hset, err := repo.LookupSet(ctx, id) + if err != nil { + return nil, nil, err + } + if hset == nil { + return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id) + } + hs = hset } - h, m, err := repo.LookupSet(ctx, id) - if err != nil { - return nil, nil, err - } - if h == nil { - return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id) - } - return h, m, nil + return hs, hl, nil } -func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (*static.HostSet, error) { +func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (host.Set, error) { const op = "host_sets.(Service).createInRepo" - var opts []static.Option - if item.GetName() != nil { - opts = append(opts, static.WithName(item.GetName().GetValue())) + if item == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item") } - if item.GetDescription() != nil { - opts = append(opts, static.WithDescription(item.GetDescription().GetValue())) + var hSet host.Set + switch host.SubtypeFromId(catalogId) { + case static.Subtype: + h, err := toStorageStaticSet(ctx, catalogId, item) + if err != nil { + return nil, err + } + repo, err := s.staticRepoFn() + if err != nil { + return nil, err + } + out, err := repo.CreateSet(ctx, scopeId, h) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.") + } + hSet = out + case plugin.Subtype: + h, err := toStoragePluginSet(ctx, catalogId, item) + repo, err := s.pluginRepoFn() + if err != nil { + return nil, err + } + out, err := repo.CreateSet(ctx, scopeId, h) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.") + } + hSet = out + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, "unrecognized catalog type") } - h, err := static.NewHostSet(catalogId, opts...) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation")) - } - repo, err := s.staticRepoFn() - if err != nil { - return nil, err - } - out, err := repo.CreateSet(ctx, scopeId, h) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set")) - } - if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.") - } - return out, nil + return hSet, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.HostSet) (*static.HostSet, []*static.Host, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId string, req *pbs.UpdateHostSetRequest) (host.Set, []host.Host, error) { const op = "host_sets.(Service).updateInRepo" - var opts []static.Option - if desc := item.GetDescription(); desc != nil { - opts = append(opts, static.WithDescription(desc.GetValue())) - } - if name := item.GetName(); name != nil { - opts = append(opts, static.WithName(name.GetValue())) - } - h, err := static.NewHostSet(catalogId, opts...) + item := req.GetItem() + h, err := toStorageStaticSet(ctx, catalogId, item) if err != nil { return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for update")) } - h.PublicId = id - dbMask := maskManager.Translate(mask) + h.PublicId = req.GetId() + dbMask := maskManager.Translate(req.GetUpdateMask().GetPaths()) if len(dbMask) == 0 { return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } @@ -442,9 +483,13 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update host set")) } if rowsUpdated == 0 { - return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", id) + return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", req.GetId()) } - return out, m, nil + var hl []host.Host + for _, h := range m { + hl = append(hl, h) + } + return out, hl, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { @@ -460,19 +505,39 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*static.HostSet, error) { - repo, err := s.staticRepoFn() - if err != nil { - return nil, err +func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Set, error) { + const op = "host_sets.(Service).listFromRepo" + var sets []host.Set + switch host.SubtypeFromId(catalogId) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return nil, err + } + sl, err := repo.ListSets(ctx, catalogId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, a := range sl { + sets = append(sets, a) + } + case plugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return nil, err + } + sl, err := repo.ListSets(ctx, catalogId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, a := range sl { + sets = append(sets, a) + } } - hl, err := repo.ListSets(ctx, catalogId) - if err != nil { - return nil, err - } - return hl, nil + return sets, nil } -func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { +func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) { const op = "host_sets.(Service).addInRepo" repo, err := s.staticRepoFn() if err != nil { @@ -489,10 +554,14 @@ func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds [ if out == nil { return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after adding hosts to it.") } - return out, m, nil + var hl []host.Host + for _, h := range m { + hl = append(hl, h) + } + return out, hl, nil } -func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { +func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) { const op = "host_sets.(Service).setInRepo" repo, err := s.staticRepoFn() if err != nil { @@ -510,10 +579,14 @@ func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds [ if out == nil { return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after setting hosts for it.") } - return out, m, nil + var hl []host.Host + for _, h := range m { + hl = append(hl, h) + } + return out, hl, nil } -func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { +func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) { const op = "host_sets.(Service).removeInRepo" repo, err := s.staticRepoFn() if err != nil { @@ -530,12 +603,22 @@ func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostId if out == nil { return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after removing hosts from it.") } - return out, m, nil + var hl []host.Host + for _, h := range m { + hl = append(hl, h) + } + return out, hl, nil } -func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (*static.HostCatalog, auth.VerifyResults) { +func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (host.Catalog, auth.VerifyResults) { res := auth.VerifyResults{} - repo, err := s.staticRepoFn() + + staticRepo, err := s.staticRepoFn() + if err != nil { + res.Error = err + return nil, res + } + pluginRepo, err := s.pluginRepoFn() if err != nil { res.Error = err return nil, res @@ -547,33 +630,66 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty case action.List, action.Create: parentId = id default: - set, _, err := repo.LookupSet(ctx, id) - if err != nil { - res.Error = err - return nil, res - } - if set == nil { - res.Error = handlers.NotFoundError() - return nil, res + var set host.Set + switch host.SubtypeFromId(id) { + case static.Subtype: + ss, _, err := staticRepo.LookupSet(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if ss == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + set = ss + case plugin.Subtype: + ps, err := pluginRepo.LookupSet(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if ps == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + set = ps } parentId = set.GetCatalogId() opts = append(opts, auth.WithId(id)) } - cat, err := repo.LookupCatalog(ctx, parentId) - if err != nil { - res.Error = err - return nil, res - } - if cat == nil { - res.Error = handlers.NotFoundError() - return nil, res + var cat host.Catalog + switch host.SubtypeFromId(id) { + case static.Subtype: + cs, err := staticRepo.LookupCatalog(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if cs == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + cat = cs + case plugin.Subtype: + pc, err := pluginRepo.LookupCatalog(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if pc == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + cat = pc } opts = append(opts, auth.WithScopeId(cat.GetScopeId()), auth.WithPin(parentId)) return cat, auth.Verify(ctx, opts...) } -func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt ...handlers.Option) (*pb.HostSet, error) { +func toProto(ctx context.Context, in host.Set, hosts []host.Host, opt ...handlers.Option) (*pb.HostSet, error) { + const op = "host_set_service.toProto" opts := handlers.GetOpts(opt...) if opts.WithOutputFields == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building hostset proto") @@ -588,7 +704,16 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt out.HostCatalogId = in.GetCatalogId() } if outputFields.Has(globals.TypeField) { - out.Type = static.Subtype.String() + switch host.SubtypeFromId(in.GetPublicId()) { + case static.Subtype: + out.Type = static.Subtype.String() + case plugin.Subtype: + out.Type = "plugin" + idParts := strings.Split(in.GetPublicId(), "_") + if len(idParts) > 2 { + out.Type = idParts[1] + } + } } if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} @@ -616,9 +741,62 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt out.HostIds = append(out.HostIds, h.GetPublicId()) } } + + if outputFields.Has(globals.AttributesField) { + switch h := in.(type) { + case *plugin.HostSet: + attrs := map[string]interface{}{} + err := json.Unmarshal(h.Attributes, &attrs) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if len(attrs) > 0 { + out.Attributes, err = structpb.NewStruct(attrs) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + } + } + return &out, nil } +func toStorageStaticSet(ctx context.Context, catalogId string, item *pb.HostSet) (*static.HostSet, error) { + const op = "host_set_service.toStorageStaticSet" + var opts []static.Option + if item.GetName() != nil { + opts = append(opts, static.WithName(item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, static.WithDescription(item.GetDescription().GetValue())) + } + hs, err := static.NewHostSet(catalogId, opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation")) + } + return hs, nil +} + +func toStoragePluginSet(ctx context.Context, catalogId string, item *pb.HostSet) (*plugin.HostSet, error) { + const op = "host_set_service.toStoragePluginSet" + var opts []plugin.Option + if item.GetName() != nil { + opts = append(opts, plugin.WithName(item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, plugin.WithDescription(item.GetDescription().GetValue())) + } + if item.GetAttributes() != nil { + opts = append(opts, plugin.WithAttributes(item.GetAttributes().AsMap())) + } + hs, err := plugin.NewHostSet(ctx, catalogId, opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation")) + } + return hs, 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 @@ -627,13 +805,25 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt // * The type asserted by the ID and/or field is known // * If relevant, the type derived from the id prefix matches what is claimed by the type field func validateGetRequest(req *pbs.GetHostSetRequest) error { - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix) + pluginPrefix := plugin.HostSetPrefix + idParts := strings.Split(req.GetId(), "_") + if len(idParts) > 2 && idParts[0] == plugin.HostSetPrefix { + pluginPrefix = strings.Join(idParts[:2], "_") + } + + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix, pluginPrefix) } func validateCreateRequest(req *pbs.CreateHostSetRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix) { + pluginPrefix := plugin.HostCatalogPrefix + idParts := strings.Split(req.GetItem().GetHostCatalogId(), "_") + if len(idParts) > 2 && idParts[0] == plugin.HostCatalogPrefix { + pluginPrefix = strings.Join(idParts[:2], "_") + } + + if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix, pluginPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) { @@ -641,6 +831,20 @@ func validateCreateRequest(req *pbs.CreateHostSetRequest) error { if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() { badFields["type"] = "Doesn't match the parent resource's type." } + case plugin.Subtype: + // TODO: Remove this check when plugin id prefixes can differ from plugin name + if req.GetItem().GetType() == "" { + break + } + idParts := strings.Split(req.GetItem().GetHostCatalogId(), "_") + if len(idParts) < 2 { + badFields["host_catalog_id"] = "The field is incorrectly formatted for a plugin id." + break + } + plgType := idParts[1] + if plgType != req.GetItem().GetType() { + badFields["type"] = "This type must match the type of the host catalog." + } } return badFields }) @@ -664,8 +868,14 @@ func validateDeleteRequest(req *pbs.DeleteHostSetRequest) error { } func validateListRequest(req *pbs.ListHostSetsRequest) error { + pluginPrefix := plugin.HostCatalogPrefix + idParts := strings.Split(req.GetHostCatalogId(), "_") + if len(idParts) > 2 && idParts[0] == plugin.HostCatalogPrefix { + pluginPrefix = strings.Join(idParts[:2], "_") + } + badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix) { + if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix, pluginPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/servers/controller/handlers/host_sets/host_set_service_test.go b/internal/servers/controller/handlers/host_sets/host_set_service_test.go index 298a54e17..355e709c8 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service_test.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service_test.go @@ -11,9 +11,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/plugin/host" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets" @@ -27,12 +29,13 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-hosts", "set-hosts", "remove-hosts"} -func TestGet(t *testing.T) { +func TestGet_Static(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -49,6 +52,9 @@ func TestGet(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] @@ -105,7 +111,7 @@ func TestGet(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetHostSetRequest) proto.Merge(req, tc.req) - s, err := host_sets.NewService(repoFn) + s, err := host_sets.NewService(repoFn, pluginRepoFn) require.NoError(err, "Couldn't create a new host set service.") got, gErr := s.GetHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) @@ -122,7 +128,8 @@ func TestGet(t *testing.T) { } } -func TestList(t *testing.T) { +func TestGet_Plugin(t *testing.T) { + t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) @@ -138,6 +145,101 @@ func TestList(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + + name := "test" + plg := host.TestPlugin(t, conn, name, name) + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + hs := plugin.TestSet(t, conn, hc.GetPublicId()) + + toMerge := &pbs.GetHostSetRequest{} + + pHost := &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Id: hs.GetPublicId(), + CreatedTime: hs.CreateTime.GetTimestamp(), + UpdatedTime: hs.UpdateTime.GetTimestamp(), + Type: name, + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + AuthorizedActions: testAuthorizedActions, + } + + cases := []struct { + name string + req *pbs.GetHostSetRequest + res *pbs.GetHostSetResponse + err error + }{ + { + name: "Get an Existing Host", + req: &pbs.GetHostSetRequest{Id: hs.GetPublicId()}, + res: &pbs.GetHostSetResponse{Item: pHost}, + }, + { + name: "Get a non existing Host Set", + req: &pbs.GetHostSetRequest{Id: static.HostSetPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Wrong id prefix", + req: &pbs.GetHostSetRequest{Id: "j_1234567890"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "space in id", + req: &pbs.GetHostSetRequest{Id: static.HostPrefix + "_1 23456789"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + req := proto.Clone(toMerge).(*pbs.GetHostSetRequest) + proto.Merge(req, tc.req) + + s, err := host_sets.NewService(repoFn, pluginRepoFn) + require.NoError(err, "Couldn't create a new host set service.") + + got, gErr := s.GetHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "GetHostSet(%+v) got error %v, wanted %v", req, gErr, tc.err) + return + } + require.NoError(gErr) + + if tc.res != nil { + tc.res.Item.Version = 1 + } + assert.Empty(cmp.Diff(got.GetItem(), tc.res.GetItem(), protocmp.Transform()), "GetHostSet(%q) got response %q, wanted %q", req, got, tc.res) + }) + } +} + +func TestList_Static(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } hcs := static.TestCatalogs(t, conn, proj.GetPublicId(), 2) hc, hcNoHosts := hcs[0], hcs[1] @@ -190,7 +292,108 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := host_sets.NewService(repoFn) + s, err := host_sets.NewService(repoFn, pluginRepoFn) + require.NoError(err, "Couldn't create new host set service.") + + // Test with non-anon user + got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test with anon user + got, gErr = s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + require.Empty(item.HostIds) + } + }) + } +} + +func TestList_Plugin(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + name := "test" + plg := host.TestPlugin(t, conn, name, name) + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + hcNoHosts := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + + var wantHs []*pb.HostSet + for i := 0; i < 10; i++ { + h := plugin.TestSet(t, conn, hc.GetPublicId()) + wantHs = append(wantHs, &pb.HostSet{ + Id: h.GetPublicId(), + HostCatalogId: h.GetCatalogId(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + CreatedTime: h.GetCreateTime().GetTimestamp(), + UpdatedTime: h.GetUpdateTime().GetTimestamp(), + Version: h.GetVersion(), + Type: name, + AuthorizedActions: testAuthorizedActions, + }) + } + + cases := []struct { + name string + req *pbs.ListHostSetsRequest + res *pbs.ListHostSetsResponse + err error + }{ + { + name: "List Many Host Sets", + req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId()}, + res: &pbs.ListHostSetsResponse{Items: wantHs}, + }, + { + name: "List No Host Sets", + req: &pbs.ListHostSetsRequest{HostCatalogId: hcNoHosts.GetPublicId()}, + res: &pbs.ListHostSetsResponse{}, + }, + { + name: "Filter To One Host Set", + req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())}, + res: &pbs.ListHostSetsResponse{Items: wantHs[1:2]}, + }, + { + name: "Filter To No Host Sets", + req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/name"=="doesnt match"`}, + res: &pbs.ListHostSetsResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := host_sets.NewService(repoFn, pluginRepoFn) require.NoError(err, "Couldn't create new host set service.") // Test with non-anon user @@ -234,10 +437,13 @@ func TestDelete(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] - s, err := host_sets.NewService(repoFn) + s, err := host_sets.NewService(repoFn, pluginRepoFn) require.NoError(t, err, "Couldn't create a new host set service.") cases := []struct { @@ -310,10 +516,13 @@ func TestDelete_twice(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] - s, err := host_sets.NewService(repoFn) + s, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(err, "Couldn't create a new host set service.") req := &pbs.DeleteHostSetRequest{ Id: h.GetPublicId(), @@ -326,7 +535,7 @@ func TestDelete_twice(t *testing.T) { assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") } -func TestCreate(t *testing.T) { +func TestCreate_Static(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -343,6 +552,9 @@ func TestCreate(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime() @@ -434,7 +646,7 @@ func TestCreate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := host_sets.NewService(repoFn) + s, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(err, "Failed to create a new host set service.") got, gErr := s.CreateHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) @@ -466,6 +678,197 @@ func TestCreate(t *testing.T) { } } +func TestCreate_Plugin(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + name := "test" + plg := host.TestPlugin(t, conn, name, name) + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + + testAttrs, err := structpb.NewStruct(map[string]interface{}{ + "int": 1, + "zero int": 0, + "string": "foo", + "zero string": "", + "bytes": []byte("bar"), + "zero bytes": nil, + "bool": true, + "zero bool": false, + "nested": map[string]interface{}{ + "int": 1, + "zero int": 0, + "string": "foo", + "zero string": "", + "bytes": []byte("bar"), + "zero bytes": nil, + "bool": true, + "zero bool": false, + }, + }) + require.NoError(t, err) + + defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime() + + cases := []struct { + name string + req *pbs.CreateHostSetRequest + res *pbs.CreateHostSetResponse + err error + }{ + { + name: "No Attributes", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Name: &wrappers.StringValue{Value: "No Attributes"}, + Description: &wrappers.StringValue{Value: "desc"}, + Type: name, + }}, + res: &pbs.CreateHostSetResponse{ + Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name), + Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + Name: &wrappers.StringValue{Value: "No Attributes"}, + Description: &wrappers.StringValue{Value: "desc"}, + Type: name, + AuthorizedActions: testAuthorizedActions, + }, + }, + }, + { + name: "With Attributes", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Name: &wrappers.StringValue{Value: "With Attributes"}, + Description: &wrappers.StringValue{Value: "desc"}, + Type: name, + Attributes: testAttrs, + }}, + res: &pbs.CreateHostSetResponse{ + Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name), + Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + Name: &wrappers.StringValue{Value: "With Attributes"}, + Description: &wrappers.StringValue{Value: "desc"}, + Type: name, + AuthorizedActions: testAuthorizedActions, + Attributes: testAttrs, + }, + }, + }, + { + name: "Create with mismatched type/name", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Name: &wrappers.StringValue{Value: "name"}, + Description: &wrappers.StringValue{Value: "desc"}, + Type: "ThisIsMadeUp", + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Create with no type", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Name: &wrappers.StringValue{Value: "no type name"}, + Description: &wrappers.StringValue{Value: "no type desc"}, + }}, + res: &pbs.CreateHostSetResponse{ + Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name), + Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + Name: &wrappers.StringValue{Value: "no type name"}, + Description: &wrappers.StringValue{Value: "no type desc"}, + Type: name, + AuthorizedActions: testAuthorizedActions, + }, + }, + }, + { + name: "Can't specify Id", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + Id: "not allowed to be set", + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + CreatedTime: timestamppb.Now(), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{ + HostCatalogId: hc.GetPublicId(), + UpdatedTime: timestamppb.Now(), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := host_sets.NewService(repoFn, plgRepoFn) + require.NoError(err, "Failed to create a new host set service.") + + got, gErr := s.CreateHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "CreateHostSet(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + if got != nil { + assert.Contains(got.GetUri(), tc.res.GetUri()) + assert.True(strings.HasPrefix(got.GetItem().GetId(), plugin.HostSetPrefix), got.GetItem().GetId()) + gotCreateTime := got.GetItem().GetCreatedTime().AsTime() + require.NoError(err, "Error converting proto to timestamp.") + gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() + require.NoError(err, "Error converting proto to timestamp") + // Verify it is a set created after the test setup's default set + assert.True(gotCreateTime.After(defaultHcCreated), "New set should have been created after default set. Was created %v, which is after %v", gotCreateTime, defaultHcCreated) + assert.True(gotUpdateTime.After(defaultHcCreated), "New set should have been updated after default set. Was updated %v, which is after %v", gotUpdateTime, defaultHcCreated) + + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + if tc.res != nil { + tc.res.Item.Version = 1 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + func TestUpdate(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -512,7 +915,10 @@ func TestUpdate(t *testing.T) { Id: hs.GetPublicId(), } - tested, err := host_sets.NewService(repoFn) + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + tested, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(t, err, "Failed to create a new host set service.") cases := []struct { @@ -848,7 +1254,10 @@ func TestAddHostSetHosts(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } - s, err := host_sets.NewService(repoFn) + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + s, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(t, err, "Error when getting new host set service.") hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] @@ -965,7 +1374,10 @@ func TestSetHostSetHosts(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } - s, err := host_sets.NewService(repoFn) + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + s, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(t, err, "Error when getting new host set service.") hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] @@ -1078,7 +1490,10 @@ func TestRemoveHostSetHosts(t *testing.T) { repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } - s, err := host_sets.NewService(repoFn) + plgRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms) + } + s, err := host_sets.NewService(repoFn, plgRepoFn) require.NoError(t, err, "Error when getting new host set service.") hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]