diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a00d5d8b..568877277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ### New and Improved +* Direct Address Targets: You can now set an address directly on a target, + bypassing the need for host catalogs, host sets and hosts. + ([PR](https://github.com/hashicorp/boundary/pull/2613)) * Custom Response Headers: Adds ability to set api and ui response headers based on status code. Includes default secure CSP and other headers. ([PR](https://github.com/hashicorp/boundary/pull/2587)) diff --git a/api/targets/option.gen.go b/api/targets/option.gen.go index 7912abea1..6d42a9aad 100644 --- a/api/targets/option.gen.go +++ b/api/targets/option.gen.go @@ -87,6 +87,18 @@ func WithRecursive(recurse bool) Option { } } +func WithAddress(inAddress string) Option { + return func(o *options) { + o.postMap["address"] = inAddress + } +} + +func DefaultAddress() Option { + return func(o *options) { + o.postMap["address"] = nil + } +} + func WithApplicationCredentialSourceIds(inApplicationCredentialSourceIds []string) Option { return func(o *options) { o.postMap["application_credential_source_ids"] = inApplicationCredentialSourceIds diff --git a/api/targets/target.gen.go b/api/targets/target.gen.go index 3dfc984be..24f789f80 100644 --- a/api/targets/target.gen.go +++ b/api/targets/target.gen.go @@ -37,6 +37,7 @@ type Target struct { InjectedApplicationCredentialSources []*CredentialSource `json:"injected_application_credential_sources,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` AuthorizedActions []string `json:"authorized_actions,omitempty"` + Address string `json:"address,omitempty"` response *api.Response } diff --git a/internal/adding-a-new-field-readme.md b/internal/adding-a-new-field-readme.md index 223257463..c191a46d0 100644 --- a/internal/adding-a-new-field-readme.md +++ b/internal/adding-a-new-field-readme.md @@ -37,8 +37,9 @@ The first step in the process is to add it to the API/SDK protobufs. * Define a `custom_options.v1.mask_mapping` tag for the field which maps the API `this` field to the storage `that` field (yes, it's the opposite of how it's defined for a storage protobuf) -* Define a `custom_options.v1.generate_sdk_option` tag for the SDK -* Define a data classification/filter tag for the field via `@gotags` +* Define a `custom_options.v1.generate_sdk_option` tag to change the SDK and add + API options (DefaultX, WithX functions). +* Define a data classification/filter tag for the field via `@gotags` which specifies how sensitive/secret/public data will be handled for the API's audit events. See the [classification rubric](./classification-rubric.md) diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index 90d702ad0..3581fee6d 100644 --- a/internal/cmd/base/dev.go +++ b/internal/cmd/base/dev.go @@ -160,7 +160,10 @@ func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error { return nil } - if _, err := b.CreateInitialTarget(ctx); err != nil { + if _, err := b.CreateInitialTargetWithAddress(ctx); err != nil { + return err + } + if _, err := b.CreateInitialTargetWithHostSources(ctx); err != nil { return err } diff --git a/internal/cmd/base/initial_resources.go b/internal/cmd/base/initial_resources.go index c9c7aef06..c0e6c522e 100644 --- a/internal/cmd/base/initial_resources.go +++ b/internal/cmd/base/initial_resources.go @@ -415,114 +415,132 @@ func (b *Server) CreateInitialHostResources(ctx context.Context) (*static.HostCa return hc, hs, h, nil } -func (b *Server) CreateInitialTarget(ctx context.Context) (target.Target, error) { +func (b *Server) CreateInitialTargetWithAddress(ctx context.Context) (target.Target, error) { rw := db.New(b.Database) kmsCache, err := kms.New(ctx, rw, rw) if err != nil { - return nil, fmt.Errorf("error creating kms cache: %w", err) + return nil, fmt.Errorf("failed to create kms cache: %w", err) } - if err := kmsCache.AddExternalWrappers( - b.Context, - kms.WithRootWrapper(b.RootKms), - ); err != nil { - return nil, fmt.Errorf("error adding config keys to kms: %w", err) + if err := kmsCache.AddExternalWrappers(ctx, kms.WithRootWrapper(b.RootKms)); err != nil { + return nil, fmt.Errorf("failed to add config keys to kms: %w", err) } targetRepo, err := target.NewRepository(ctx, rw, rw, kmsCache) if err != nil { - return nil, fmt.Errorf("error creating target repository: %w", err) + return nil, fmt.Errorf("failed to create target repository: %w", err) } - // Host Catalog - if b.DevTargetId == "" { + // When this function is not called as part of boundary dev (eg: as part of + // boundary database init or tests), generate random target ids. + if len(b.DevTargetId) == 0 { b.DevTargetId, err = db.NewPublicId(tcp.TargetPrefix) if err != nil { - return nil, fmt.Errorf("error generating initial target id: %w", err) + return nil, fmt.Errorf("failed to generate initial target id: %w", err) } } if b.DevTargetDefaultPort == 0 { b.DevTargetDefaultPort = 22 } + if len(b.DevTargetAddress) == 0 { + b.DevTargetAddress = "127.0.0.1" + } opts := []target.Option{ - target.WithName("Generated target"), - target.WithDescription("Provides an initial target in Boundary"), + target.WithName("Generated target with a direct address"), + target.WithDescription("Provides an initial target using an address in Boundary"), target.WithDefaultPort(uint32(b.DevTargetDefaultPort)), target.WithSessionMaxSeconds(uint32(b.DevTargetSessionMaxSeconds)), target.WithSessionConnectionLimit(int32(b.DevTargetSessionConnectionLimit)), target.WithPublicId(b.DevTargetId), + target.WithAddress(b.DevTargetAddress), } t, err := target.New(ctx, tcp.Subtype, b.DevProjectId, opts...) if err != nil { - return nil, fmt.Errorf("error creating in memory target: %w", err) + return nil, fmt.Errorf("failed to create target object: %w", err) } tt, _, _, err := targetRepo.CreateTarget(ctx, t, opts...) if err != nil { - return nil, fmt.Errorf("error saving target to the db: %w", err) + return nil, fmt.Errorf("failed to save target to the db: %w", err) } - tt, _, _, err = targetRepo.AddTargetHostSources(ctx, tt.GetPublicId(), tt.GetVersion(), []string{b.DevHostSetId}) - if err != nil { - return nil, fmt.Errorf("error saving target to the db: %w", err) - } - b.InfoKeys = append(b.InfoKeys, "generated target id") - b.Info["generated target id"] = b.DevTargetId + b.InfoKeys = append(b.InfoKeys, "generated target with address id") + b.Info["generated target with address id"] = b.DevTargetId - // If we have an unprivileged dev user, add user to the role that grants - // list/read:self/cancel:self on sessions, read:self/delete:self/list on - // tokens, and an authorize-session role if b.DevUnprivilegedUserId != "" { iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) if err != nil { - return nil, fmt.Errorf("unable to create repo for unprivileged user target connection role: %w", err) + return nil, fmt.Errorf("failed to create iam repository: %w", err) } + err = unprivilegedDevUserRoleSetup(ctx, iamRepo, b.DevUnprivilegedUserId, b.DevProjectId, b.DevTargetId) + if err != nil { + return nil, fmt.Errorf("failed to set up unprivileged dev user: %w", err) + } + } - roles, err := iamRepo.ListRoles(ctx, []string{b.DevProjectId}) - if err != nil { - return nil, fmt.Errorf("unable to list existing roles in project: %w", err) - } - if len(roles) != 2 { - return nil, fmt.Errorf("unexpected number of roles in default project, expected 2, got %d", len(roles)) - } - var idx int = -1 - for i, r := range roles { - // Hacky, I know, but saves a DB trip to look up other - // characteristics like "if any principals are currently attached". - // No matter what we pick here it's a bit heuristical. - if r.Name == "Default Grants" { - idx = i - break - } - } - if idx == -1 { - return nil, fmt.Errorf("couldn't find default grants role in default project") - } - if _, err := iamRepo.AddPrincipalRoles(ctx, - roles[idx].PublicId, - roles[idx].Version, - []string{b.DevUnprivilegedUserId}); err != nil { - return nil, fmt.Errorf("error adding unpriv user ID to project default role: %w", err) - } + return tt, nil +} - pr, err := iam.NewRole(b.DevProjectId, - iam.WithName("Unprivileged User Session Authorization"), - iam.WithDescription(`Provides grants within the dev project scope to allow the initial unprivileged user to authorize sessions against the dev target`), - ) +func (b *Server) CreateInitialTargetWithHostSources(ctx context.Context) (target.Target, error) { + rw := db.New(b.Database) + + kmsCache, err := kms.New(ctx, rw, rw) + if err != nil { + return nil, fmt.Errorf("failed to create kms cache: %w", err) + } + if err := kmsCache.AddExternalWrappers( + b.Context, + kms.WithRootWrapper(b.RootKms), + ); err != nil { + return nil, fmt.Errorf("failed to add config keys to kms: %w", err) + } + + targetRepo, err := target.NewRepository(ctx, rw, rw, kmsCache) + if err != nil { + return nil, fmt.Errorf("failed to create target repository: %w", err) + } + + // When this function is not called as part of boundary dev (eg: as part of + // boundary database init or tests), generate random target ids. + if len(b.DevSecondaryTargetId) == 0 { + b.DevSecondaryTargetId, err = db.NewPublicId(tcp.TargetPrefix) if err != nil { - return nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) + return nil, fmt.Errorf("failed to generate initial secondary target id: %w", err) } - sessionRole, err := iamRepo.CreateRole(ctx, pr) + } + if b.DevTargetDefaultPort == 0 { + b.DevTargetDefaultPort = 22 + } + + opts := []target.Option{ + target.WithName("Generated target using host sources"), + target.WithDescription("Provides a target using host sources in Boundary"), + target.WithDefaultPort(uint32(b.DevTargetDefaultPort)), + target.WithSessionMaxSeconds(uint32(b.DevTargetSessionMaxSeconds)), + target.WithSessionConnectionLimit(int32(b.DevTargetSessionConnectionLimit)), + target.WithPublicId(b.DevSecondaryTargetId), + } + t, err := target.New(ctx, tcp.Subtype, b.DevProjectId, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create target object: %w", err) + } + tt, _, _, err := targetRepo.CreateTarget(ctx, t, opts...) + if err != nil { + return nil, fmt.Errorf("failed to save target to the db: %w", err) + } + tt, _, _, err = targetRepo.AddTargetHostSources(ctx, tt.GetPublicId(), tt.GetVersion(), []string{b.DevHostSetId}) + if err != nil { + return nil, fmt.Errorf("failed to add host source %q to target: %w", b.DevHostSetId, err) + } + b.InfoKeys = append(b.InfoKeys, "generated target with host source id") + b.Info["generated target with host source id"] = b.DevSecondaryTargetId + + if b.DevUnprivilegedUserId != "" { + iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) if err != nil { - return nil, fmt.Errorf("error creating role for unprivileged user generated grants: %w", err) + return nil, fmt.Errorf("failed to create iam repository: %w", err) } - if _, err := iamRepo.AddRoleGrants(ctx, - sessionRole.PublicId, - sessionRole.Version, - []string{fmt.Sprintf("id=%s;actions=authorize-session", b.DevTargetId)}, - ); err != nil { - return nil, fmt.Errorf("error creating grant for unprivileged user generated grants: %w", err) - } - if _, err := iamRepo.AddPrincipalRoles(ctx, sessionRole.PublicId, sessionRole.Version+1, []string{b.DevUnprivilegedUserId}, nil); err != nil { - return nil, fmt.Errorf("error adding principal to role for unprivileged user generated grants: %w", err) + err = unprivilegedDevUserRoleSetup(ctx, iamRepo, b.DevUnprivilegedUserId, b.DevProjectId, b.DevSecondaryTargetId) + if err != nil { + return nil, fmt.Errorf("failed to set up unprivileged dev user: %w", err) } } @@ -576,3 +594,79 @@ func (b *Server) RegisterHostPlugin(ctx context.Context, name string, plg plgpb. return plugin, nil } + +// unprivilegedDevUserRoleSetup adds dev user to the role that grants +// list/read:self/cancel:self on sessions and read:self/delete:self/list on +// tokens. It also creates a role with an `authorize-session` grant for the +// provided targetId. +func unprivilegedDevUserRoleSetup(ctx context.Context, repo *iam.Repository, userId, projectId, targetId string) error { + roles, err := repo.ListRoles(ctx, []string{projectId}) + if err != nil { + return fmt.Errorf("failed to list existing roles for project id %q: %w", projectId, err) + } + + // Look for default grants role to set unprivileged user as a principal. + defaultRoleIdx := -1 + for i, r := range roles { + // Hacky, I know, but saves a DB trip to look up other + // characteristics like "if any principals are currently attached". + // No matter what we pick here it's a bit heuristical. + if r.Name == "Default Grants" { + defaultRoleIdx = i + break + } + } + if defaultRoleIdx == -1 { + return fmt.Errorf("couldn't find default grants role for project id %q", projectId) + } + defaultRole := roles[defaultRoleIdx] + + // This function may be called more than once for the same boundary + // deployment (eg: if we're creating more than one target), so we need to + // check if the unprivileged user is not already a principal for the default + // role in this project, as attempting to add an existing principal is an + // error. + principals, err := repo.ListPrincipalRoles(ctx, defaultRole.GetPublicId()) + if err != nil { + return fmt.Errorf("failed to list principals for default project role: %w", err) + } + found := false + for _, p := range principals { + if p.PrincipalId == userId { + found = true + } + } + if !found { + _, err = repo.AddPrincipalRoles(ctx, defaultRole.GetPublicId(), defaultRole.GetVersion(), []string{userId}) + if err != nil { + return fmt.Errorf("failed to add %q as principal for role id %q", userId, defaultRole.GetPublicId()) + } + defaultRole.Version++ // The above call increments the role version in the database, so we must also track that with our state. + } + + // Create a new role for the "authorize-session" grant and add the + // unprivileged user as a principal. + asRole, err := iam.NewRole(projectId, + iam.WithName(fmt.Sprintf("Session authorization for %s", targetId)), + iam.WithDescription(fmt.Sprintf("Provides grants within the dev project scope to allow the initial unprivileged user to authorize sessions against %s", targetId)), + ) + if err != nil { + return fmt.Errorf("failed to create role object: %w", err) + } + + asRole, err = repo.CreateRole(ctx, asRole) + if err != nil { + return fmt.Errorf("failed to create role for unprivileged user: %w", err) + } + if _, err := repo.AddPrincipalRoles(ctx, asRole.GetPublicId(), asRole.GetVersion(), []string{userId}, nil); err != nil { + return fmt.Errorf("failed to add unprivileged user as principal to new role: %w", err) + } + asRole.Version++ + + _, err = repo.AddRoleGrants(ctx, asRole.GetPublicId(), asRole.GetVersion(), []string{fmt.Sprintf("id=%s;actions=authorize-session", targetId)}) + if err != nil { + return fmt.Errorf("failed to add authorize-session grant for unprivileged user: %w", err) + } + + return nil +} diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index 14afcf016..ff9f5dc8e 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -106,8 +106,10 @@ type Server struct { DevHostCatalogId string DevHostSetId string DevHostId string - DevTargetId string - DevHostAddress string + DevTargetId string // Target using address. + DevSecondaryTargetId string // Target using host sources. + DevHostAddress string // Host address for target using host sources. + DevTargetAddress string // Network address for target with address. DevTargetDefaultPort int DevTargetSessionMaxSeconds int DevTargetSessionConnectionLimit int diff --git a/internal/cmd/commands/connect/ssh.go b/internal/cmd/commands/connect/ssh.go index ea8f994ae..cb0da3617 100644 --- a/internal/cmd/commands/connect/ssh.go +++ b/internal/cmd/commands/connect/ssh.go @@ -63,7 +63,16 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, _ string, creds credentials) case "tcp": // SSH detects a host key change when the localhost proxy port changes // This uses the host ID instead of 'localhost:port'. - args = append(args, "-o", fmt.Sprintf("HostKeyAlias=%s", c.sessionAuthzData.GetHostId())) + if len(c.sessionAuthzData.GetHostId()) > 0 { + args = append(args, "-o", fmt.Sprintf("HostKeyAlias=%s", c.sessionAuthzData.GetHostId())) + } else { + // In cases where the Target has no host sources and has an + // address directly attached to it, we have no Host Id. Use + // Target Id instead. Only one address can ever be present on a + // target, and no other host sources may be present at the same + // time, so this is a reasonable alternative. + args = append(args, "-o", fmt.Sprintf("HostKeyAlias=%s", c.sessionAuthzData.GetTargetId())) + } case "ssh": args = append(args, "-o", "NoHostAuthenticationForLocalhost=yes") } diff --git a/internal/cmd/commands/database/init.go b/internal/cmd/commands/database/init.go index 1c83407fe..b02ee3ec3 100644 --- a/internal/cmd/commands/database/init.go +++ b/internal/cmd/commands/database/init.go @@ -426,26 +426,42 @@ func (c *InitCommand) Run(args []string) (retCode int) { } c.DevTargetSessionConnectionLimit = -1 - t, err := c.CreateInitialTarget(c.Context) + ta, err := c.CreateInitialTargetWithAddress(c.Context) if err != nil { c.UI.Error(fmt.Errorf("Error creating initial target: %w", err).Error()) return base.CommandCliError } + taInfo := &TargetInfo{ + TargetId: ta.GetPublicId(), + DefaultPort: ta.GetDefaultPort(), + SessionMaxSeconds: ta.GetSessionMaxSeconds(), + SessionConnectionLimit: ta.GetSessionConnectionLimit(), + Type: string(ta.GetType()), + ScopeId: ta.GetProjectId(), + Name: ta.GetName(), + } - targetInfo := &TargetInfo{ - TargetId: c.DevTargetId, - DefaultPort: t.GetDefaultPort(), - SessionMaxSeconds: t.GetSessionMaxSeconds(), - SessionConnectionLimit: t.GetSessionConnectionLimit(), - Type: "tcp", - ScopeId: c.DevProjectId, - Name: t.GetName(), + ths, err := c.CreateInitialTargetWithHostSources(c.Context) + if err != nil { + c.UI.Error(fmt.Errorf("Error creating initial secondary target: %w", err).Error()) + return base.CommandCliError + } + thsInfo := &TargetInfo{ + TargetId: ths.GetPublicId(), + DefaultPort: ths.GetDefaultPort(), + SessionMaxSeconds: ths.GetSessionMaxSeconds(), + SessionConnectionLimit: ths.GetSessionConnectionLimit(), + Type: string(ths.GetType()), + ScopeId: ths.GetProjectId(), + Name: ths.GetName(), } switch base.Format(c.UI) { case "table": - c.UI.Output(generateInitialTargetTableOutput(targetInfo)) + c.UI.Output(generateInitialTargetTableOutput(taInfo)) + c.UI.Output(generateInitialTargetTableOutput(thsInfo)) case "json": - jsonMap["target"] = targetInfo + jsonMap["target"] = taInfo + jsonMap["target_secondary"] = thsInfo } return base.CommandSuccess diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index 7193106c0..b4ce7e3fb 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -66,6 +66,7 @@ type Command struct { flagUnprivilegedLoginName string flagUnprivilegedPassword string flagIdSuffix string + flagSecondaryIdSuffix string flagHostAddress string flagTargetDefaultPort int flagTargetSessionMaxSeconds int @@ -141,7 +142,15 @@ func (c *Command) Flags() *base.FlagSets { Target: &c.flagIdSuffix, Default: "1234567890", EnvVar: "BOUNDARY_DEV_ID_SUFFIX", - Usage: `If set, auto-created resources will use this value for their identifier (along with their resource-specific prefix). Must be 10 alphanumeric characters. As an example, if this is set to "1234567890", the generated password auth method ID will be "ampw_1234567890", the generated TCP target ID will be "ttcp_1234567890", and so on.`, + Usage: `If set, auto-created resources will use this value for their identifier (along with their resource-specific prefix). Must be 10 alphanumeric characters. As an example, if this is set to "1234567890", the generated password auth method ID will be "ampw_1234567890", the generated TCP target ID will be "ttcp_1234567890", and so on. Must be different from -secondary-id-suffix (BOUNDARY_DEV_SECONDARY_ID_SUFFIX).`, + }) + + f.StringVar(&base.StringVar{ + Name: "secondary-id-suffix", + Target: &c.flagSecondaryIdSuffix, + Default: "0987654321", + EnvVar: "BOUNDARY_DEV_SECONDARY_ID_SUFFIX", + Usage: `If set, secondary auto-created resources will use this value for their identifier (along with their resource-specific prefix). Must be 10 alphanumeric characters. Currently only used for the target resource. Must be different from -id-suffix (BOUNDARY_DEV_ID_SUFFIX).`, }) f.StringVar(&base.StringVar{ @@ -450,6 +459,23 @@ func (c *Command) Run(args []string) int { c.DevHostId = fmt.Sprintf("%s_%s", static.HostPrefix, c.flagIdSuffix) c.DevTargetId = fmt.Sprintf("%s_%s", tcp.TargetPrefix, c.flagIdSuffix) } + if c.flagSecondaryIdSuffix != "" { + if len(c.flagSecondaryIdSuffix) != 10 { + c.UI.Error("Invalid secondary ID suffix, must be exactly 10 characters") + return base.CommandUserError + } + if !handlers.ValidId(handlers.Id("abc_"+c.flagSecondaryIdSuffix), "abc") { + c.UI.Error("Invalid secondary ID suffix, must be in the set A-Za-z0-9") + return base.CommandUserError + } + c.DevSecondaryTargetId = fmt.Sprintf("%s_%s", tcp.TargetPrefix, c.flagSecondaryIdSuffix) + } + + if c.flagIdSuffix != "" && c.flagSecondaryIdSuffix != "" && + strings.EqualFold(c.flagIdSuffix, c.flagSecondaryIdSuffix) { + c.UI.Error("Primary and secondary ID suffixes are equal, must be distinct") + return base.CommandUserError + } host, port, err := net.SplitHostPort(c.flagHostAddress) if err != nil { diff --git a/internal/cmd/commands/server/listener_reload_test.go b/internal/cmd/commands/server/listener_reload_test.go index 27fa5efa4..9e40c47cb 100644 --- a/internal/cmd/commands/server/listener_reload_test.go +++ b/internal/cmd/commands/server/listener_reload_test.go @@ -99,7 +99,7 @@ func TestServer_ReloadListener(t *testing.T) { CreateDevDatabase: true, ControllerKey: controllerKey, UseDevAuthMethod: true, - UseDevTarget: true, + UseDevTargets: true, }) // Unset auto-created KMSes that are overwritten by config on startup cmd.RootKms = nil diff --git a/internal/cmd/commands/server/server_test.go b/internal/cmd/commands/server/server_test.go index 2454f09a2..de1d9a940 100644 --- a/internal/cmd/commands/server/server_test.go +++ b/internal/cmd/commands/server/server_test.go @@ -14,15 +14,18 @@ import ( // We try to pull these from TestController, but targets et al are // computed off of a suffix instead of having constants. Just define // for now here, this can be tweaked later if need be. -const defaultTestTargetId = "ttcp_1234567890" +const ( + defaultTestTargetId = "ttcp_1234567890" + defaultSecondaryTestTargetId = "ttcp_0987654321" -const rootKmsConfig = ` + rootKmsConfig = ` kms "aead" { purpose = "root" aead_type = "aes-gcm" key = "%s" key_id = "global_root" }` +) type testServerCommandOpts struct { // Whether or not to create the dev database @@ -34,8 +37,8 @@ type testServerCommandOpts struct { // Use the well-known dev mode auth method id (1234567890) UseDevAuthMethod bool - // Use the well-known dev mode target method id (1234567890) - UseDevTarget bool + // Use the well-known dev mode target method ids (1234567890 and 0987654321) + UseDevTargets bool // Whether or not to enable metric collection. If enable metrics will use // prometheus' default registerer. @@ -71,8 +74,9 @@ func testServerCommand(t *testing.T, opts testServerCommandOpts) *Command { cmd.Server.DevPassword = controller.DefaultTestPassword } - if opts.UseDevTarget { + if opts.UseDevTargets { cmd.Server.DevTargetId = defaultTestTargetId + cmd.Server.DevSecondaryTargetId = defaultSecondaryTestTargetId } err = cmd.CreateDevDatabase(cmd.Context, base.WithDatabaseTemplate("boundary_template"), base.WithSkipOidcAuthMethodCreation()) diff --git a/internal/cmd/commands/server/worker_shutdown_reload_test.go b/internal/cmd/commands/server/worker_shutdown_reload_test.go index 773cedfe1..d99556d2a 100644 --- a/internal/cmd/commands/server/worker_shutdown_reload_test.go +++ b/internal/cmd/commands/server/worker_shutdown_reload_test.go @@ -78,9 +78,10 @@ func TestServer_ShutdownWorker(t *testing.T) { tcl := targets.NewClient(client) tgtL, err := tcl.List(ctx, scope.Global.String(), targets.WithRecursive(true)) require.NoError(err) - require.Len(tgtL.Items, 1) + require.Len(tgtL.Items, 2) tgt := tgtL.Items[0] require.NotNil(tgt) + require.NotNil(tgtL.GetItems()[1]) // Create test server, update default port on target ts := helper.NewTestTcpServer(t) diff --git a/internal/cmd/commands/targetscmd/funcs.go b/internal/cmd/commands/targetscmd/funcs.go index 52db2d3a0..1dd562e42 100644 --- a/internal/cmd/commands/targetscmd/funcs.go +++ b/internal/cmd/commands/targetscmd/funcs.go @@ -482,6 +482,11 @@ func (c *Command) printListTable(items []*targets.Target) string { fmt.Sprintf(" Description: %s", item.Description), ) } + if item.Address != "" { + output = append(output, + fmt.Sprintf(" Address: %s", item.Address), + ) + } if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", @@ -516,6 +521,9 @@ func printItemTable(item *targets.Target, resp *api.Response) string { if item.Description != "" { nonAttributeMap["Description"] = item.Description } + if item.Address != "" { + nonAttributeMap["Address"] = item.Address + } if item.WorkerFilter != "" { nonAttributeMap["Worker Filter"] = item.WorkerFilter } diff --git a/internal/cmd/commands/targetscmd/ssh_funcs.go b/internal/cmd/commands/targetscmd/ssh_funcs.go index 13de54a3d..073e96d8c 100644 --- a/internal/cmd/commands/targetscmd/ssh_funcs.go +++ b/internal/cmd/commands/targetscmd/ssh_funcs.go @@ -19,8 +19,8 @@ func init() { func extraSshActionsFlagsMapFuncImpl() map[string][]string { return map[string][]string{ - "create": {"default-port", "session-max-seconds", "session-connection-limit", "egress-worker-filter", "ingress-worker-filter"}, - "update": {"default-port", "session-max-seconds", "session-connection-limit", "worker-filter", "egress-worker-filter", "ingress-worker-filter"}, + "create": {"address", "default-port", "session-max-seconds", "session-connection-limit", "egress-worker-filter", "ingress-worker-filter"}, + "update": {"address", "default-port", "session-max-seconds", "session-connection-limit", "worker-filter", "egress-worker-filter", "ingress-worker-filter"}, } } @@ -31,6 +31,7 @@ type extraSshCmdVars struct { flagWorkerFilter string flagEgressWorkerFilter string flagIngressWorkerFilter string + flagAddress string } func (c *SshCommand) extraSshHelpFunc(helpMap map[string]func() string) string { @@ -66,6 +67,12 @@ func extraSshFlagsFuncImpl(c *SshCommand, set *base.FlagSets, f *base.FlagSet) { for _, name := range flagsSshMap[c.Func] { switch name { + case "address": + fs.StringVar(&base.StringVar{ + Name: "address", + Target: &c.flagAddress, + Usage: "Optionally, a valid network address to connect to for this target. Can not be used alongside host sources.", + }) case "default-port": fs.StringVar(&base.StringVar{ Name: "default-port", @@ -188,6 +195,14 @@ func extraSshFlagsHandlingFuncImpl(c *SshCommand, _ *base.FlagSets, opts *[]targ *opts = append(*opts, targets.WithIngressWorkerFilter(c.flagIngressWorkerFilter)) } + switch c.flagAddress { + case "": + case "null": + *opts = append(*opts, targets.DefaultAddress()) + default: + *opts = append(*opts, targets.WithAddress(c.flagAddress)) + } + return true } diff --git a/internal/cmd/commands/targetscmd/tcp_funcs.go b/internal/cmd/commands/targetscmd/tcp_funcs.go index e09053a99..7f08e0191 100644 --- a/internal/cmd/commands/targetscmd/tcp_funcs.go +++ b/internal/cmd/commands/targetscmd/tcp_funcs.go @@ -18,8 +18,8 @@ func init() { func extraTcpActionsFlagsMapFuncImpl() map[string][]string { return map[string][]string{ - "create": {"default-port", "session-max-seconds", "session-connection-limit", "egress-worker-filter", "ingress-worker-filter"}, - "update": {"default-port", "session-max-seconds", "session-connection-limit", "worker-filter", "egress-worker-filter", "ingress-worker-filter"}, + "create": {"address", "default-port", "session-max-seconds", "session-connection-limit", "egress-worker-filter", "ingress-worker-filter"}, + "update": {"address", "default-port", "session-max-seconds", "session-connection-limit", "worker-filter", "egress-worker-filter", "ingress-worker-filter"}, } } @@ -30,6 +30,7 @@ type extraTcpCmdVars struct { flagWorkerFilter string flagEgressWorkerFilter string flagIngressWorkerFilter string + flagAddress string } func (c *TcpCommand) extraTcpHelpFunc(helpMap map[string]func() string) string { @@ -65,6 +66,12 @@ func extraTcpFlagsFuncImpl(c *TcpCommand, set *base.FlagSets, f *base.FlagSet) { for _, name := range flagsTcpMap[c.Func] { switch name { + case "address": + fs.StringVar(&base.StringVar{ + Name: "address", + Target: &c.flagAddress, + Usage: "Optionally, a valid network address to connect to for this target. Can not be used alongside host sources.", + }) case "default-port": fs.StringVar(&base.StringVar{ Name: "default-port", @@ -187,5 +194,13 @@ func extraTcpFlagsHandlingFuncImpl(c *TcpCommand, _ *base.FlagSets, opts *[]targ *opts = append(*opts, targets.WithIngressWorkerFilter(c.flagIngressWorkerFilter)) } + switch c.flagAddress { + case "": + case "null": + *opts = append(*opts, targets.DefaultAddress()) + default: + *opts = append(*opts, targets.WithAddress(c.flagAddress)) + } + return true } diff --git a/internal/daemon/controller/handlers/errors.go b/internal/daemon/controller/handlers/errors.go index 352d3b554..4b89d2e12 100644 --- a/internal/daemon/controller/handlers/errors.go +++ b/internal/daemon/controller/handlers/errors.go @@ -150,6 +150,22 @@ func InvalidArgumentErrorf(msg string, fields map[string]string) *ApiError { return apiErr } +// ConflictErrorf generates an ApiErr when a pre-conditional check is violated. +// Note, this deliberately doesn't translate to the similarly named '412 +// Precondition Failed' HTTP response status. The ApiErr returned is a 400 bad +// request because this is how the grpc-gateway mapping is implemented for +// failed precondition protobuf errors. +func ConflictErrorf(msg string) *ApiError { + const op = "handlers.ConflictErrorf" + ctx := context.TODO() + err := ApiErrorWithCodeAndMessage(codes.FailedPrecondition, msg) + var apiErr *ApiError + if !errors.As(err, &apiErr) { + event.WriteError(ctx, op, err, event.WithInfoMsg("Unable to build conflict api error.")) + } + return apiErr +} + var statusRegEx = regexp.MustCompile("Status: ([0-9]+), Kind: \"(.*)\", Error: \"(.*)\"") // Converts a known errors into an error that can presented to an end user over the API. @@ -184,6 +200,8 @@ func backendErrorToApiError(inErr error) *ApiError { return InvalidArgumentErrorf("Error in provided request", map[string]string{"update_mask": "Invalid update mask provided."}) case errors.IsUniqueError(inErr): return InvalidArgumentErrorf(genericUniquenessMsg, nil) + case errors.IsConflictError(inErr): + return ConflictErrorf(inErr.Error()) } var statusCode int32 = http.StatusInternalServerError diff --git a/internal/daemon/controller/handlers/sessions/session_service_test.go b/internal/daemon/controller/handlers/sessions/session_service_test.go index a5e953d5f..0ef67a751 100644 --- a/internal/daemon/controller/handlers/sessions/session_service_test.go +++ b/internal/daemon/controller/handlers/sessions/session_service_test.go @@ -721,6 +721,7 @@ func TestCancel(t *testing.T) { requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx := auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, gErr := s.CancelSession(ctx, tc.req) + if tc.err != nil { require.Error(gErr) // It's hard to mix and match api/error package errors right now @@ -764,6 +765,8 @@ func TestCancel(t *testing.T) { tc.res.GetItem().ExpirationTime = got.GetItem().GetExpirationTime() } + assert.Equal(got.GetItem().HostId, tc.res.GetItem().HostId) + assert.Equal(got.GetItem().HostSetId, tc.res.GetItem().HostSetId) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CancelSession(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) if tc.req != nil { diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 17c0500c7..4f1d1f8d1 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -781,6 +781,10 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession endpoints = append(endpoints, eps...) } + if len(endpoints) == 0 && t.GetAddress() == "" { + return nil, handlers.NotFoundErrorf("No host sources or address found for given target.") + } + var chosenEndpoint *host.Endpoint if requestedId != "" { for _, ep := range endpoints { @@ -798,25 +802,32 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession } } - if chosenEndpoint == nil { - if len(endpoints) == 0 { - // No hosts were found, error - return nil, handlers.NotFoundErrorf("No endpoint found from available target host sources.") - } + var h, p, hostId, hostSetId string + if chosenEndpoint == nil && len(endpoints) > 0 { chosenEndpoint = endpoints[rand.Intn(len(endpoints))] } - h, p, err := net.SplitHostPort(chosenEndpoint.Address) - switch { - case err != nil && strings.Contains(err.Error(), missingPortErrStr): - if t.GetDefaultPort() == 0 { - return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("neither the selected host %q nor the target provides a port to use", chosenEndpoint.HostId)) + if chosenEndpoint != nil { + hostId = chosenEndpoint.HostId + hostSetId = chosenEndpoint.SetId + h, p, err = net.SplitHostPort(chosenEndpoint.Address) + switch { + case err != nil && strings.Contains(err.Error(), missingPortErrStr): + if t.GetDefaultPort() == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("neither the selected host %q nor the target provides a port to use", chosenEndpoint.HostId)) + } + h = chosenEndpoint.Address + p = strconv.FormatUint(uint64(t.GetDefaultPort()), 10) + case err != nil: + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error when parsing the chosen endpoints host address")) } - h = chosenEndpoint.Address - p = strconv.FormatUint(uint64(t.GetDefaultPort()), 10) - case err != nil: - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error when parsing the chosen endpoints host address")) } + + if t.GetAddress() != "" && hostId == "" && hostSetId == "" { + h = t.GetAddress() + p = strconv.FormatUint(uint64(t.GetDefaultPort()), 10) + } + // Generate the endpoint URL endpointUrl := &url.URL{ Scheme: t.GetType().String(), @@ -857,9 +868,9 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession expTime.Seconds += int64(t.GetSessionMaxSeconds()) sessionComposition := session.ComposedOf{ UserId: authResults.UserId, - HostId: chosenEndpoint.HostId, + HostId: hostId, TargetId: t.GetPublicId(), - HostSetId: chosenEndpoint.SetId, + HostSetId: hostSetId, AuthTokenId: authResults.AuthTokenId, ProjectId: authResults.Scope.Id, Endpoint: endpointUrl.String(), @@ -871,7 +882,6 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession DynamicCredentials: dynCreds, StaticCredentials: staticCreds, } - sess, err := session.New(sessionComposition) if err != nil { return nil, err @@ -977,7 +987,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession Type: t.GetType().String(), Certificate: sess.Certificate, PrivateKey: sess.CertificatePrivateKey, - HostId: chosenEndpoint.HostId, + HostId: hostId, Endpoint: endpointUrl.String(), WorkerInfo: wl.WorkerList(selectedWorkers).WorkerInfos(), ConnectionLimit: t.GetSessionConnectionLimit(), @@ -996,8 +1006,8 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession Type: t.GetType().String(), AuthorizationToken: encodedMarshaledSad, UserId: authResults.UserId, - HostId: chosenEndpoint.HostId, - HostSetId: chosenEndpoint.SetId, + HostId: hostId, + HostSetId: hostSetId, Endpoint: endpointUrl.String(), Credentials: creds, } @@ -1040,6 +1050,9 @@ func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Targ if item.GetIngressWorkerFilter() != nil { opts = append(opts, target.WithIngressWorkerFilter(item.GetIngressWorkerFilter().GetValue())) } + if item.GetAddress() != nil { + opts = append(opts, target.WithAddress(item.GetAddress().GetValue())) + } attr, err := subtypeRegistry.newAttribute(target.SubtypeFromType(item.GetType()), item.GetAttrs()) if err != nil { @@ -1067,6 +1080,7 @@ func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Targ 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())) @@ -1090,6 +1104,10 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st 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(item.GetAddress().GetValue())) + } subtype := target.SubtypeFromId(id) attr, err := subtypeRegistry.newAttribute(subtype, item.GetAttrs()) @@ -1113,8 +1131,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update target")) } - - dbMask := maskManager.Translate(mask) + 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."}) } @@ -1122,7 +1139,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st if err != nil { return nil, nil, nil, err } - out, hs, cl, rowsUpdated, err := repo.UpdateTarget(ctx, u, version, dbMask) + out, hs, cl, 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")) } @@ -1167,8 +1184,14 @@ func (s Service) addHostSourcesInRepo(ctx context.Context, targetId string, host } out, hs, cl, 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, 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, "Unable to add host sources to target: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Failed to add target host sources") } if out == nil { return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding host sources to it.") @@ -1184,8 +1207,14 @@ func (s Service) setHostSourcesInRepo(ctx context.Context, targetId string, host } _, _, _, 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, "Unable to set host sources in target: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Failed to set target host sources") } out, hs, cl, err := repo.LookupTarget(ctx, targetId) @@ -1424,6 +1453,9 @@ func toProto(ctx context.Context, in target.Target, hostSources []target.HostSou }) } } + if outputFields.Has(globals.AddressField) { + out.Address = wrapperspb.String(in.GetAddress()) + } var brokeredSources, injectedAppSources []*pb.CredentialSource var brokeredSourceIds, injectedAppSourceIds []string @@ -1525,7 +1557,20 @@ func validateCreateRequest(req *pbs.CreateTargetRequest) error { badFields[globals.IngressWorkerFilterField] = err.Error() } } - + if address := req.GetItem().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(), missingPortErrStr): + default: + badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err) + } + } subtype := target.SubtypeFromType(req.GetItem().GetType()) _, err := subtypeRegistry.get(subtype) if err != nil { @@ -1588,6 +1633,20 @@ func validateUpdateRequest(req *pbs.UpdateTargetRequest) error { badFields[globals.IngressWorkerFilterField] = err.Error() } } + if address := req.GetItem().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(), missingPortErrStr): + default: + badFields[globals.AddressField] = fmt.Sprintf("Error parsing address: %v.", err) + } + } subtype := target.SubtypeFromId(req.GetId()) _, err := subtypeRegistry.get(subtype) if err != nil { diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index 840b819c8..fc7fa2ba0 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -139,6 +139,8 @@ func TestGet(t *testing.T) { ctx := context.Background() tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "test", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})) + tarAddr := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "test address", target.WithAddress("8.8.8.8")) + pTar := &pb.Target{ Id: tar.GetPublicId(), ScopeId: proj.GetPublicId(), @@ -152,11 +154,27 @@ func TestGet(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(28800), SessionConnectionLimit: wrapperspb.Int32(-1), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, } for _, ihs := range hs { pTar.HostSources = append(pTar.HostSources, &pb.HostSource{Id: ihs.GetPublicId(), HostCatalogId: ihs.GetCatalogId()}) } + pTarAddr := &pb.Target{ + Id: tarAddr.GetPublicId(), + ScopeId: proj.GetPublicId(), + Name: wrapperspb.String("test address"), + CreatedTime: tarAddr.GetCreateTime().GetTimestamp(), + UpdatedTime: tarAddr.GetUpdateTime().GetTimestamp(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{Value: "8.8.8.8"}, + } + cases := []struct { name string req *pbs.GetTargetRequest @@ -168,6 +186,11 @@ func TestGet(t *testing.T) { req: &pbs.GetTargetRequest{Id: tar.GetPublicId()}, res: &pbs.GetTargetResponse{Item: pTar}, }, + { + name: "Get an Existing Target w/ address", + req: &pbs.GetTargetRequest{Id: tarAddr.GetPublicId()}, + res: &pbs.GetTargetResponse{Item: pTarAddr}, + }, { name: "Get a non existing Target", req: &pbs.GetTargetRequest{Id: tcp.TargetPrefix + "_DoesntExis"}, @@ -272,6 +295,7 @@ func TestList(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(28800), SessionConnectionLimit: wrapperspb.Int32(-1), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }) totalTars = append(totalTars, wantTars[i]) tar = tcp.TestTarget(ctx, t, conn, otherProj.GetPublicId(), name, target.WithHostSources([]string{otherHss[0].GetPublicId(), otherHss[1].GetPublicId()})) @@ -288,6 +312,7 @@ func TestList(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(28800), SessionConnectionLimit: wrapperspb.Int32(-1), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }) } @@ -575,6 +600,7 @@ func TestCreate(t *testing.T) { SessionConnectionLimit: wrapperspb.Int32(-1), AuthorizedActions: testAuthorizedActions, EgressWorkerFilter: wrapperspb.String(`type == "bar"`), + Address: &wrapperspb.StringValue{}, }, }, }, @@ -668,6 +694,30 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Invalid address length", + req: &pbs.CreateTargetRequest{Item: &pb.Target{ + Address: wrapperspb.String("ab"), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid address w/ port", + req: &pbs.CreateTargetRequest{Item: &pb.Target{ + Address: wrapperspb.String("8.8.8.8:80"), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid address not parsable", + req: &pbs.CreateTargetRequest{Item: &pb.Target{ + Address: wrapperspb.String("aaa.8bc.8.8:80:abc"), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -824,6 +874,7 @@ func TestUpdate(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(3600), SessionConnectionLimit: wrapperspb.Int32(5), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }, }, }, @@ -858,6 +909,7 @@ func TestUpdate(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(tar.GetSessionMaxSeconds()), SessionConnectionLimit: wrapperspb.Int32(tar.GetSessionConnectionLimit()), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }, }, }, @@ -955,6 +1007,7 @@ func TestUpdate(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(tar.GetSessionMaxSeconds()), SessionConnectionLimit: wrapperspb.Int32(tar.GetSessionConnectionLimit()), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }, }, }, @@ -988,6 +1041,7 @@ func TestUpdate(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(tar.GetSessionMaxSeconds()), SessionConnectionLimit: wrapperspb.Int32(tar.GetSessionConnectionLimit()), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }, }, }, @@ -1021,6 +1075,7 @@ func TestUpdate(t *testing.T) { SessionMaxSeconds: wrapperspb.UInt32(tar.GetSessionMaxSeconds()), SessionConnectionLimit: wrapperspb.Int32(tar.GetSessionConnectionLimit()), AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, }, }, }, @@ -1109,6 +1164,45 @@ func TestUpdate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Invalid address length", + req: &pbs.UpdateTargetRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"address"}, + }, + Item: &pb.Target{ + Address: wrapperspb.String("ab"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid address w/ port", + req: &pbs.UpdateTargetRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"address"}, + }, + Item: &pb.Target{ + Address: wrapperspb.String("8.8.8.8:80"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid address not parsable", + req: &pbs.UpdateTargetRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"address"}, + }, + Item: &pb.Target{ + Address: wrapperspb.String("aaa.8bc.8.8:80:abc"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/daemon/controller/handlers/targets/tcp/tcp.go b/internal/daemon/controller/handlers/targets/tcp/tcp.go index f379fda13..e3968bdcd 100644 --- a/internal/daemon/controller/handlers/targets/tcp/tcp.go +++ b/internal/daemon/controller/handlers/targets/tcp/tcp.go @@ -5,8 +5,9 @@ import ( "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/store" "github.com/hashicorp/boundary/internal/target/tcp" - "github.com/hashicorp/boundary/internal/target/tcp/store" + tcpStore "github.com/hashicorp/boundary/internal/target/tcp/store" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" ) @@ -78,7 +79,7 @@ func init() { var err error if maskManager, err = handlers.NewMaskManager( - handlers.MaskDestination{&store.Target{}}, + handlers.MaskDestination{&tcpStore.Target{}, &store.TargetAddress{}}, handlers.MaskSource{&pb.Target{}, &pb.TcpTargetAttributes{}}, ); err != nil { panic(err) diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 34f5de30f..6df00fb47 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -709,7 +709,7 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, t.Fatal(err) } if !opts.DisableTargetCreation { - if _, err := tc.b.CreateInitialTarget(ctx); err != nil { + if _, err := tc.b.CreateInitialTargetWithHostSources(ctx); err != nil { t.Fatal(err) } } diff --git a/internal/db/schema/migrations/oss/postgres/16/04_wh_credential_dimension.up.sql b/internal/db/schema/migrations/oss/postgres/16/04_wh_credential_dimension.up.sql index 3a28f709d..1c702f82c 100644 --- a/internal/db/schema/migrations/oss/postgres/16/04_wh_credential_dimension.up.sql +++ b/internal/db/schema/migrations/oss/postgres/16/04_wh_credential_dimension.up.sql @@ -10,6 +10,7 @@ begin; alter column credential_group_key drop default; -- replaces function from 15/01_wh_rename_key_columns.up.sql + -- replaced function in 70/03_wh_sessions.up.sql drop trigger wh_insert_session on session; drop function wh_insert_session; diff --git a/internal/db/schema/migrations/oss/postgres/26/03_wh_network_address_dimensions.up.sql b/internal/db/schema/migrations/oss/postgres/26/03_wh_network_address_dimensions.up.sql index 8f75a781d..5ef8e2318 100644 --- a/internal/db/schema/migrations/oss/postgres/26/03_wh_network_address_dimensions.up.sql +++ b/internal/db/schema/migrations/oss/postgres/26/03_wh_network_address_dimensions.up.sql @@ -70,6 +70,7 @@ begin; -- replaces function from 15/02_wh_rename_key_columns.up.sql + -- replaced function in 70/03_wh_sessions.up.sql -- adds the network address key to the host dimension table. drop function wh_upsert_host; create function wh_upsert_host(p_host_id wt_public_id, p_host_set_id wt_public_id, p_target_id wt_public_id) returns wh_dim_key diff --git a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql index ca024fd98..20a63251f 100644 --- a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql @@ -82,6 +82,7 @@ begin; -- warehouse -- Replaces whx_host_dimension_source defined in 26/02_wh_network_address_dimensions.up.sql. + -- Replaced view in 70/03_wh_sessions.up.sql drop view whx_host_dimension_source; create view whx_host_dimension_source as select -- id is the first column in the target view diff --git a/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql b/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql index 996cca65a..5b154c730 100644 --- a/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql @@ -35,6 +35,7 @@ begin; $$ language plpgsql; -- Replaces trigger from 01/50_session.up.sql + -- Replaced trigger in 70/02_sessions.up.sql create or replace function insert_session() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql index 35368fe9c..699a77d17 100644 --- a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql +++ b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql @@ -32,6 +32,7 @@ begin; on update cascade; -- Replaces trigger from 44/04_session.up.sql + -- Replaced in 70/02_sessions.up.sql -- Adding a foreign key reference to the kms key means we have -- to set the key_id to null when the scope_id is set to null, -- otherwise we can't delete scopes with sessions, since deleting diff --git a/internal/db/schema/migrations/oss/postgres/59/01_target_ingress_egress_worker_filters.up.sql b/internal/db/schema/migrations/oss/postgres/59/01_target_ingress_egress_worker_filters.up.sql index a2bd621d3..347364f83 100644 --- a/internal/db/schema/migrations/oss/postgres/59/01_target_ingress_egress_worker_filters.up.sql +++ b/internal/db/schema/migrations/oss/postgres/59/01_target_ingress_egress_worker_filters.up.sql @@ -86,6 +86,7 @@ select public_id, from target_tcp; -- Replaces view from 56/06_add_session_private_key_column.up.sql +-- Replaced in 70/02_sessions.up.sql drop view session_list; create view session_list as select diff --git a/internal/db/schema/migrations/oss/postgres/70/01_targets.up.sql b/internal/db/schema/migrations/oss/postgres/70/01_targets.up.sql new file mode 100644 index 000000000..5feca7f3c --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/70/01_targets.up.sql @@ -0,0 +1,27 @@ +begin; + + create table target_address ( + target_id wt_public_id primary key, + address text not null + -- the following constraints are used to validate the length requirements of a dns name + -- note the address column can also contain other network types such as ipv4 + constraint address_must_be_more_than_2_characters + check(length(trim(address)) > 2) + constraint address_must_be_less_than_256_characters + check(length(trim(address)) < 256), + constraint target_fkey foreign key (target_id) + references target (public_id) + on delete cascade + on update cascade + ); + comment on table target_address is + 'target_address entries represent a network address assigned to a target.'; + + create trigger immutable_columns before update on target_address + for each row execute function immutable_columns('target_id'); + + insert into oplog_ticket (name, version) + values + ('target_address', 1); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/70/02_sessions.up.sql b/internal/db/schema/migrations/oss/postgres/70/02_sessions.up.sql new file mode 100644 index 000000000..1b7b12a51 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/70/02_sessions.up.sql @@ -0,0 +1,218 @@ +begin; + + -- Replaces trigger from 56/02_add_data_key_foreign_key_references.up.sql + drop trigger cancel_session_with_null_fk on session; + create or replace function cancel_session_with_null_fk() returns trigger + as $$ + begin + -- Note that we need each of these to run in case + -- more than one of them is null. + if new.auth_token_id is null then + perform cancel_session(new.public_id); + end if; + if new.project_id is null then + -- Setting the key_id to null will allow the scope + -- to cascade delete its keys. + new.key_id = null; + perform cancel_session(new.public_id); + end if; + if new.target_id is null then + perform cancel_session(new.public_id); + end if; + if new.user_id is null then + perform cancel_session(new.public_id); + end if; + return new; + end; + $$ language plpgsql; + + create trigger cancel_session_with_null_fk before update of auth_token_id, project_id, target_id, user_id on session + for each row execute procedure cancel_session_with_null_fk(); + + create table session_target_address ( + session_id wt_public_id primary key, + target_id wt_public_id, + -- the following foreign key constraint is set to null on a delete + -- because we want to be able to invoke a trigger to call cancel_session + -- when the references column is deleted + -- rather than canceling a session for any reason that may cause a row to be deleted. + constraint target_address_fkey foreign key (target_id) + references target_address (target_id) + on delete set null + on update cascade, + constraint session_fkey foreign key (session_id) + references session (public_id) + on delete cascade + on update cascade, + constraint session_target_address_session_id_target_id_uq + unique(session_id, target_id) + ); + comment on table session_target_address is + 'session_target_address entries represent a session that is using a network address that is assigned directly to a Target.'; + + create trigger immutable_columns before update on session_target_address + for each row execute procedure immutable_columns('session_id'); + + create function cancel_session_with_null_target_address_fk() returns trigger + as $$ + begin + if new.target_id is null then + perform cancel_session(new.session_id); + delete from session_target_address where session_id = new.session_id; + end if; + return new; + end; + $$ language plpgsql; + + create trigger cancel_session_with_null_target_address_fk after update of target_id on session_target_address + for each row execute procedure cancel_session_with_null_target_address_fk(); + + create function insert_session_target_address() returns trigger + as $$ + begin + if new.target_id is null then + raise exception 'target_id is null'; + end if; + return new; + end; + $$ language plpgsql; + + create trigger insert_session_target_address before insert on session_target_address + for each row execute procedure insert_session_target_address(); + + create table session_host_set_host ( + session_id wt_public_id primary key, + host_set_id wt_public_id, + host_id wt_public_id, + -- the following foreign key constraints for host_set_id & host_id is set to null on a delete + -- because we want to be able to invoke a trigger to call cancel_session + -- when the references column is deleted + -- rather than canceling a session for any reason that may cause a row to be deleted. + constraint host_set_fkey foreign key (host_set_id) + references host_set (public_id) + on delete set null + on update cascade, + constraint host_fkey foreign key (host_id) + references host (public_id) + on delete set null + on update cascade, + constraint session_fkey foreign key (session_id) + references session (public_id) + on delete cascade + on update cascade, + constraint session_host_set_host_session_id_host_set_id_host_id_uq + unique(session_id, host_set_id, host_id) + ); + comment on table session_host_set_host is + 'session_host_set entries represent a session that is using a Host Set.'; + + create trigger immutable_columns before update on session_host_set_host + for each row execute procedure immutable_columns('session_id'); + + create function cancel_session_with_null_host_source_fk() returns trigger + as $$ + begin + if new.host_set_id is null or new.host_id is null then + perform cancel_session(new.session_id); + delete from session_host_set_host where session_id = new.session_id; + end if; + return new; + end; + $$ language plpgsql; + + create trigger cancel_session_with_null_host_source_fk after update of host_set_id, host_id on session_host_set_host + for each row execute procedure cancel_session_with_null_host_source_fk(); + + create function insert_session_host_set_host() returns trigger + as $$ + begin + if new.host_set_id is null then + raise exception 'host_set_id is null'; + end if; + if new.host_id is null then + raise exception 'host_id is null'; + end if; + return new; + end; + $$ language plpgsql; + + create trigger insert_session_host_set_host before insert on session_host_set_host + for each row execute procedure insert_session_host_set_host(); + + drop view session_list; + alter table session + drop constraint session_host_id_fkey, + drop constraint session_host_set_id_fkey, + drop column host_id, + drop column host_set_id + ; + + -- Replaces trigger from 44/04_sessions.up.sql + drop trigger insert_session on session; + create or replace function insert_session() returns trigger + as $$ + begin + case + when new.user_id is null then + raise exception 'user_id is null'; + when new.target_id is null then + raise exception 'target_id is null'; + when new.auth_token_id is null then + raise exception 'auth_token_id is null'; + when new.project_id is null then + raise exception 'project_id is null'; + when new.endpoint is null then + raise exception 'endpoint is null'; + else + end case; + return new; + end; + $$ language plpgsql; + + create trigger insert_session before insert on session + for each row execute procedure insert_session(); + + -- Replaces view from 59/01_target_ingress_egress_worker_filters.up.sql + create view session_list as + select + s.public_id, + s.user_id, + shsh.host_id, + s.target_id, + shsh.host_set_id, + s.auth_token_id, + s.project_id, + s.certificate, + s.certificate_private_key, + s.expiration_time, + s.connection_limit, + s.tofu_token, + s.key_id, + s.termination_reason, + s.version, + s.create_time, + s.update_time, + s.endpoint, + s.worker_filter, + s.egress_worker_filter, + s.ingress_worker_filter, + ss.state, + ss.previous_end_time, + ss.start_time, + ss.end_time, + sc.public_id as connection_id, + sc.client_tcp_address, + sc.client_tcp_port, + sc.endpoint_tcp_address, + sc.endpoint_tcp_port, + sc.bytes_up, + sc.bytes_down, + sc.closed_reason + from session s + join session_state ss on + s.public_id = ss.session_id + left join session_connection sc on + s.public_id = sc.session_id + left join session_host_set_host shsh on s.public_id = shsh.session_id; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/70/03_wh_sessions.up.sql b/internal/db/schema/migrations/oss/postgres/70/03_wh_sessions.up.sql new file mode 100644 index 000000000..d0353ec31 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/70/03_wh_sessions.up.sql @@ -0,0 +1,424 @@ +begin; + + -- replaced view in 44/03_targets.up.sql + drop view whx_host_dimension_source; + create view whx_host_dimension_source as + with + host_sources ( + host_id, host_type, host_name, host_description, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description + ) as ( + select + h.public_id as host_id, + case when sh.public_id is not null then 'static host' + when ph.public_id is not null then 'plugin host' + else 'Unknown' end as host_type, + case when sh.public_id is not null then coalesce(sh.name, 'None') + when ph.public_id is not null then coalesce(ph.name, 'None') + else 'Unknown' end as host_name, + case when sh.public_id is not null then coalesce(sh.description, 'None') + when ph.public_id is not null then coalesce(ph.description, 'None') + else 'Unknown' end as host_description, + hs.public_id as host_set_id, + case when shs.public_id is not null then 'static host set' + when phs.public_id is not null then 'plugin host set' + else 'Unknown' end as host_set_type, + case + when shs.public_id is not null then coalesce(shs.name, 'None') + when phs.public_id is not null then coalesce(phs.name, 'None') + else 'None' + end as host_set_name, + case + when shs.public_id is not null then coalesce(shs.description, 'None') + when phs.public_id is not null then coalesce(phs.description, 'None') + else 'None' + end as host_set_description, + hc.public_id as host_catalog_id, + case when shc.public_id is not null then 'static host catalog' + when phc.public_id is not null then 'plugin host catalog' + else 'Unknown' end as host_catalog_type, + case + when shc.public_id is not null then coalesce(shc.name, 'None') + when phc.public_id is not null then coalesce(phc.name, 'None') + else 'None' + end as host_catalog_name, + case + when shc.public_id is not null then coalesce(shc.description, 'None') + when phc.public_id is not null then coalesce(phc.description, 'None') + else 'None' + end as host_catalog_description, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(t.name, 'None') as target_name, + coalesce(t.description, 'None') as target_description, + coalesce(t.default_port, 0) as target_default_port_number, + t.session_max_seconds as target_session_max_seconds, + t.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from host as h + join host_catalog as hc on h.catalog_id = hc.public_id + join host_set as hs on h.catalog_id = hs.catalog_id + join target_host_set as ts on hs.public_id = ts.host_set_id + join target_tcp as t on ts.target_id = t.public_id + join iam_scope as p on t.project_id = p.public_id and p.type = 'project' + join iam_scope as o on p.parent_id = o.public_id and o.type = 'org' + + left join static_host as sh on sh.public_id = h.public_id + left join host_plugin_host as ph on ph.public_id = h.public_id + left join static_host_catalog as shc on shc.public_id = hc.public_id + left join host_plugin_catalog as phc on phc.public_id = hc.public_id + left join static_host_set as shs on shs.public_id = hs.public_id + left join host_plugin_set as phs on phs.public_id = hs.public_id + ), + host_target_address ( + host_id, host_type, host_name, host_description, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description + ) as ( + select + 'Not Applicable' as host_id, + 'direct address' as host_type, + 'Not Applicable' as host_name, + 'Not Applicable' as host_description, + 'Not Applicable' as host_set_id, + 'Not Applicable' as host_set_type, + 'Not Applicable' as host_set_name, + 'Not Applicable' as host_set_description, + 'Not Applicable' as host_catalog_id, + 'Not Applicable' as host_catalog_type, + 'Not Applicable' as host_catalog_name, + 'Not Applicable' as host_catalog_description, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(t.name, 'None') as target_name, + coalesce(t.description, 'None') as target_description, + coalesce(t.default_port, 0) as target_default_port_number, + t.session_max_seconds as target_session_max_seconds, + t.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from target_tcp as t + right join target_address as ta on t.public_id = ta.target_id + left join iam_scope as p on p.public_id = t.project_id + left join iam_scope as o on o.public_id = p.parent_id + ) + select * from host_sources + union + select * from host_target_address; + + -- replaced function in 26/03_wh_network_address_dimensions.up.sql + create or replace function wh_upsert_host() returns trigger + as $$ + declare + p_target_id wt_public_id; + p_host_key wh_dim_key; + src whx_host_dimension_target%rowtype; + target whx_host_dimension_target%rowtype; + addr_group_key wh_dim_key; + begin + select target_id into p_target_id + from session + where session.public_id = new.session_id; + + if p_target_id is null then + raise exception 'target_id is null'; + end if; + + select * into target + from whx_host_dimension_target as t + where t.host_id = new.host_id + and t.host_set_id = new.host_set_id + and t.target_id = p_target_id; + + select wh_upsert_network_address_dimension(new.host_id) into addr_group_key; + + select target.key, addr_group_key, t.* into src + from whx_host_dimension_source as t + where t.host_id = new.host_id + and t.host_set_id = new.host_set_id + and t.target_id = p_target_id; + + if src is distinct from target then + + -- expire the current row + update wh_host_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where host_id = new.host_id + and host_set_id = new.host_set_id + and target_id = p_target_id + and current_row_indicator = 'Current'; + + -- insert a new row + insert into wh_host_dimension ( + host_id, host_type, host_name, host_description, + network_address_group_key, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select host_id, host_type, host_name, host_description, + addr_group_key, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_host_dimension_source + where host_id = new.host_id + and host_set_id = new.host_set_id + and target_id = p_target_id; + + end if; + + select key into p_host_key + from wh_host_dimension as t + where t.current_row_indicator = 'Current' + and t.host_id = new.host_id + and t.host_set_id = new.host_set_id + and t.target_id = p_target_id; + + update wh_session_accumulating_fact + set host_key = p_host_key + where session_id = new.session_id; + + return new; + end; + $$ language plpgsql; + + create trigger wh_update_session_connection_accumulating_fact after insert on session_host_set_host + for each row execute procedure wh_upsert_host(); + + -- replaced function in 16/04_wh_credential_dimension.up.sql + create or replace function wh_insert_session() returns trigger + as $$ + declare + new_row wh_session_accumulating_fact%rowtype; + begin + with + pending_timestamp (date_dim_key, time_dim_key, ts) as ( + select wh_date_key(start_time), wh_time_key(start_time), start_time + from session_state + where session_id = new.public_id + and state = 'pending' + ) + insert into wh_session_accumulating_fact ( + session_id, + auth_token_id, + host_key, + user_key, + credential_group_key, + session_pending_date_key, + session_pending_time_key, + session_pending_time + ) + select new.public_id, + new.auth_token_id, + 'no host source', -- will be updated by wh_upsert_host + wh_upsert_user(new.user_id, new.auth_token_id), + 'no credentials', -- will be updated by wh_upsert_credential_group + pending_timestamp.date_dim_key, + pending_timestamp.time_dim_key, + pending_timestamp.ts + from pending_timestamp + returning * into strict new_row; + return null; + end; + $$ language plpgsql; + + insert into wh_host_dimension ( + key, + host_id, host_type, host_name, host_description, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time, network_address_group_key + ) + values + ( + 'no host source', + 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', -1, -1, -1, + '00000000000', 'None', 'None', '00000000000', 'None', 'None', + 'Current', now(), 'infinity'::timestamptz, 'Unknown' + ); + + create function wh_upsert_direct_network_address_dimension(p_address text, p_target_id text) returns wh_dim_key + as $$ + declare + p_address_type text := 'Not Applicable'; + p_ip_address_family text := 'Not Applicable'; + p_private_ip_address_indicator text := 'Not Applicable'; + p_dns_name text := 'Not Applicable'; + p_ip4_address text := 'Not Applicable'; + p_ip6_address text := 'Not Applicable'; + nag_key wh_dim_key; + begin + if wh_try_cast_inet(p_address) is null then + p_address_type := 'DNS Name'; + p_dns_name := p_address; + else + p_address_type := 'IP Address'; + p_private_ip_address_indicator := wh_private_address_indicator(wh_try_cast_inet(p_address)); + end if; + + if family(wh_try_cast_inet(p_address)) = 4 then + p_ip_address_family := 'IPv4'; + p_ip4_address := host(wh_try_cast_inet(p_address)); + elsif family(wh_try_cast_inet(p_address)) = 6 then + p_ip_address_family := 'IPv6'; + p_ip6_address := host(wh_try_cast_inet(p_address)); + end if; + + insert into wh_network_address_dimension + (address, address_type, ip_address_family, private_ip_address_indicator, dns_name, ip4_address, ip6_address) + values + (p_address, p_address_type, p_ip_address_family, p_private_ip_address_indicator, p_dns_name, p_ip4_address, p_ip6_address) on conflict do nothing; + + select distinct g.network_address_group_key into nag_key + from wh_network_address_group_membership g + left join wh_host_dimension h on g.network_address_group_key = h.network_address_group_key + where g.network_address = p_address + and h.current_row_indicator = 'Current' + and h.target_id = p_target_id + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable'; + + if nag_key is null then + select network_address_group_key into nag_key + from wh_host_dimension h + where h.current_row_indicator = 'Current' + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.target_id = p_target_id; + end if; + + if nag_key is null then + insert into wh_network_address_group default values returning key into nag_key; + end if; + + insert into wh_network_address_group_membership + (network_address_group_key, network_address) + values + (nag_key, p_address) on conflict do nothing; + + return nag_key; + end + $$ language plpgsql; + + create function wh_upsert_host_direct_network_address() returns trigger + as $$ + declare + p_address text; + src whx_host_dimension_target%rowtype; + target whx_host_dimension_target%rowtype; + addr_group_key wh_dim_key; + p_host_key wh_dim_key; + begin + select address into p_address + from target_address + where target_address.target_id = new.target_id; + + if p_address is null then + raise exception 'target address is null'; + end if; + + select * into target + from whx_host_dimension_target as t + where t.host_id = 'Not Applicable' + and t.host_set_id = 'Not Applicable' + and t.target_id = new.target_id; + + select wh_upsert_direct_network_address_dimension(p_address, new.target_id) into addr_group_key; + + select target.key, addr_group_key, t.* into src + from whx_host_dimension_source as t + where t.host_id = 'Not Applicable' + and t.host_set_id = 'Not Applicable' + and t.target_id = new.target_id; + + if src is distinct from target then + + -- expire the current row + update wh_host_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where host_id = 'Not Applicable' + and host_set_id = 'Not Applicable' + and target_id = new.target_id + and current_row_indicator = 'Current'; + + -- insert a new row + insert into wh_host_dimension ( + host_id, host_type, host_name, host_description, + network_address_group_key, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select host_id, host_type, host_name, host_description, + addr_group_key, + host_set_id, host_set_type, host_set_name, host_set_description, + host_catalog_id, host_catalog_type, host_catalog_name, host_catalog_description, + target_id, target_type, target_name, target_description, + target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_host_dimension_source + where host_id = 'Not Applicable' + and host_set_id = 'Not Applicable' + and target_id = new.target_id; + + end if; + + select key into p_host_key + from wh_host_dimension as t + where t.current_row_indicator = 'Current' + and t.host_id = 'Not Applicable' + and t.host_set_id = 'Not Applicable' + and t.target_id = new.target_id; + + update wh_session_accumulating_fact + set host_key = p_host_key + where session_id = new.session_id; + + return new; + end; + $$ language plpgsql; + + create trigger wh_update_session_connection_accumulating_fact after insert on session_target_address + for each row execute procedure wh_upsert_host_direct_network_address(); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres_59_01_test.go b/internal/db/schema/migrations/oss/postgres_59_01_test.go deleted file mode 100644 index 35d731ef8..000000000 --- a/internal/db/schema/migrations/oss/postgres_59_01_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package oss_test - -import ( - "context" - "crypto/rand" - "testing" - - "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/db/common" - "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/iam" - "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/target" - "github.com/hashicorp/boundary/internal/target/targettest" - "github.com/hashicorp/boundary/internal/types/scope" - "github.com/hashicorp/boundary/testing/dbtest" - "github.com/stretchr/testify/require" -) - -const ( - insertTargetQuery = `INSERT INTO "target_tcp" ("public_id","project_id","name","session_max_seconds","session_connection_limit","worker_filter") VALUES ($1,$2,$3,28800,-1,$4)` -) - -func TestMigrations_AddEgressAndIngressFilters(t *testing.T) { - const ( - priorMigration = 58001 - currentMigration = 59001 - ) - - t.Parallel() - ctx := context.Background() - dialect := dbtest.Postgres - - c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, c()) - }) - d, err := common.SqlOpen(dialect, u) - require.NoError(t, err) - - // migration to the prior migration (before the one we want to test) - m, err := schema.NewManager(ctx, schema.Dialect(dialect), d, schema.WithEditions( - schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": priorMigration}), - )) - require.NoError(t, err) - - _, err = m.ApplyMigrations(ctx) - require.NoError(t, err) - state, err := m.CurrentState(ctx) - require.NoError(t, err) - want := &schema.State{ - Initialized: true, - Editions: []schema.EditionState{ - { - Name: "oss", - BinarySchemaVersion: priorMigration, - DatabaseSchemaVersion: priorMigration, - DatabaseSchemaState: schema.Equal, - }, - }, - } - require.Equal(t, want, state) - - // Get a connection - dbType, err := db.StringToDbType(dialect) - require.NoError(t, err) - conn, err := db.Open(ctx, dbType, u) - require.NoError(t, err) - - wrapper := db.TestWrapper(t) - rw := db.New(conn) - kmsCache := kms.TestKms(t, conn, wrapper) - err = kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) - require.NoError(t, err) - - iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj := iam.TestScopes(t, iamRepo) - - // Create a target with a worker filter - oldTargetId1, err := db.NewPublicId("ttcp") - oldWorkerFilter := "foo==bar" - require.NoError(t, err) - execResult, err := d.ExecContext(ctx, insertTargetQuery, oldTargetId1, proj.PublicId, "old-target-1", oldWorkerFilter) - require.NoError(t, err) - rowsAffected, err := execResult.RowsAffected() - require.NoError(t, err) - require.Equal(t, int64(1), rowsAffected) - - // Create another target with a worker filter to use in testing - oldTargetId2, err := db.NewPublicId("ttcp") - require.NoError(t, err) - execResult, err = d.ExecContext(ctx, insertTargetQuery, oldTargetId2, proj.PublicId, "old-target-2", oldWorkerFilter) - require.NoError(t, err) - rowsAffected, err = execResult.RowsAffected() - require.NoError(t, err) - require.Equal(t, int64(1), rowsAffected) - - // now we're ready for the migration we want to test. - m, err = schema.NewManager(ctx, schema.Dialect(dialect), d, schema.WithEditions( - schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": currentMigration}), - )) - require.NoError(t, err) - - _, err = m.ApplyMigrations(ctx) - require.NoError(t, err) - state, err = m.CurrentState(ctx) - require.NoError(t, err) - want = &schema.State{ - Initialized: true, - Editions: []schema.EditionState{ - { - Name: "oss", - BinarySchemaVersion: currentMigration, - DatabaseSchemaVersion: currentMigration, - DatabaseSchemaState: schema.Equal, - }, - }, - } - require.Equal(t, want, state) - - testRepo, err := target.NewRepository(ctx, rw, rw, kmsCache) - - // Ensure old target 1 still have the same worker filter - result, _, _, err := testRepo.LookupTarget(ctx, oldTargetId1) - require.Equal(t, oldWorkerFilter, result.GetWorkerFilter()) - - // Update the old worker filter - newWorkerFilter := "bar==foo" - result.SetWorkerFilter(newWorkerFilter) - result, _, _, _, err = testRepo.UpdateTarget(ctx, result, result.GetVersion(), []string{"WorkerFilter"}) - require.Equal(t, newWorkerFilter, result.GetWorkerFilter()) - - // Update to set an egress filter- expect worker filter to be cleared out - result.SetEgressWorkerFilter(newWorkerFilter) - result, _, _, _, err = testRepo.UpdateTarget(ctx, result, result.GetVersion(), []string{"EgressWorkerFilter"}) - require.Equal(t, newWorkerFilter, result.GetEgressWorkerFilter()) - require.Empty(t, result.GetWorkerFilter()) - - // Attempt to set worker filter again - expect failure - result.SetWorkerFilter(newWorkerFilter) - result, _, _, _, err = testRepo.UpdateTarget(ctx, result, result.GetVersion(), []string{"WorkerFilter"}) - require.Error(t, err) - - // Ensure old target 2 still has the same worker filter - result, _, _, err = testRepo.LookupTarget(ctx, oldTargetId2) - require.Equal(t, oldWorkerFilter, result.GetWorkerFilter()) - - // Clear out its worker filter - result.SetWorkerFilter("") - result, _, _, _, err = testRepo.UpdateTarget(ctx, result, result.GetVersion(), []string{"WorkerFilter"}) - require.Equal(t, "", result.GetWorkerFilter()) - - // Update to set an ingress filter- expect worker filter to be cleared out - result.SetIngressWorkerFilter(newWorkerFilter) - result, _, _, _, err = testRepo.UpdateTarget(ctx, result, result.GetVersion(), []string{"IngressWorkerFilter"}) - require.Equal(t, newWorkerFilter, result.GetIngressWorkerFilter()) - require.Empty(t, result.GetWorkerFilter()) - - // Attempt to create a new target with a worker filter- expect failure - tar, err := targettest.New(proj.PublicId, target.WithName("new-worker"), target.WithWorkerFilter(oldWorkerFilter)) - require.NoError(t, err) - id, err := db.NewPublicId("ttcp") - require.NoError(t, err) - tar.SetPublicId(ctx, id) - err = rw.Create(context.Background(), tar) - require.Error(t, err) - - // Create a new target with egress and ingress filters - tar2, err := targettest.New(proj.PublicId, target.WithName("new-worker-filters"), target.WithEgressWorkerFilter(oldWorkerFilter), - target.WithIngressWorkerFilter(oldWorkerFilter)) - require.NoError(t, err) - id, err = db.NewPublicId("ttcp") - require.NoError(t, err) - tar2.SetPublicId(ctx, id) - err = rw.Create(context.Background(), tar2) - require.NoError(t, err) - - // Attempt to create a new target with all 3 filters- expect failure - tar3, err := targettest.New(proj.PublicId, target.WithName("new-worker-filters"), target.WithEgressWorkerFilter(oldWorkerFilter), - target.WithIngressWorkerFilter(oldWorkerFilter), target.WithEgressWorkerFilter(oldWorkerFilter)) - require.NoError(t, err) - id, err = db.NewPublicId("ttcp") - require.NoError(t, err) - tar3.SetPublicId(ctx, id) - err = rw.Create(context.Background(), tar3) - require.Error(t, err) -} diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index 5ace45326..ad2217285 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -10,7 +10,8 @@ begin; (parent_id, type, public_id, name) values ('o_____colors', 'project', 'p____bcolors', 'Blue Color Mill'), - ('o_____colors', 'project', 'p____rcolors', 'Red Color Mill'); + ('o_____colors', 'project', 'p____rcolors', 'Red Color Mill'), + ('o_____colors', 'project', 'p____gcolors', 'Green Color Mill'); -- Add global users insert into iam_user @@ -27,7 +28,8 @@ begin; ('o_____colors', 'u______clare', 'Clare'), ('o_____colors', 'u______cindy', 'Cindy'), ('o_____colors', 'u______carly', 'Carly'), - ('o_____colors', 'u______ciara', 'Ciara'); + ('o_____colors', 'u______ciara', 'Ciara'), + ('o_____colors', 'u_______cora', 'Cora'); -- Add keys insert into kms_root_key @@ -56,7 +58,8 @@ begin; ('global', 'g___gg-group', 'Global Group'), ('o_____colors', 'g___oc-group', 'Colors R Us Group'), ('p____bcolors', 'g___cb-group', 'Blue Color Group'), - ('p____rcolors', 'g___cr-group', 'Red Color Group'); + ('p____rcolors', 'g___cr-group', 'Red Color Group'), + ('p____gcolors', 'g___cg-group', 'Green Color Group'); insert into iam_group_member_user (group_id, member_id) @@ -64,15 +67,18 @@ begin; ('g___gg-group', 'u_______gary'), ('g___oc-group', 'u______clare'), ('g___cb-group', 'u______cindy'), - ('g___cr-group', 'u______carly'); + ('g___cr-group', 'u______carly'), + ('g___cg-group', 'u_______cora'); insert into iam_role (scope_id, grant_scope_id, public_id, name) values ('p____bcolors', 'p____bcolors', 'r_pp_bc__mix', 'Color Mixer'), ('p____rcolors', 'p____rcolors', 'r_pp_rc__mix', 'Color Mixer'), + ('p____gcolors', 'p____gcolors', 'r_pp_gc__mix', 'Color Mixer'), ('o_____colors', 'p____bcolors', 'r_op_bc__art', 'Blue Color Artist'), ('o_____colors', 'p____rcolors', 'r_op_rc__art', 'Red Color Artist'), + ('o_____colors', 'p____gcolors', 'r_op_gc__art', 'Green Color Artist'), ('o_____colors', 'o_____colors', 'r_oo_____art', 'Color Artist'), ('global', 'o_____colors', 'r_go____name', 'Color Namer'), ('global', 'p____bcolors', 'r_gp____spec', 'Blue Color Inspector'), @@ -89,15 +95,18 @@ begin; ('r_oo_____art', 'type=color;action=create', 'create color'), ('r_op_bc__art', 'type=color;action=create', 'create color'), ('r_op_rc__art', 'type=color;action=create', 'create color'), + ('r_op_gc__art', 'type=color;action=create', 'create color'), ('r_pp_bc__mix', 'type=color;action=mix', 'mix color'), - ('r_pp_rc__mix', 'type=color;action=mix', 'mix color'); + ('r_pp_rc__mix', 'type=color;action=mix', 'mix color'), + ('r_pp_gc__mix', 'type=color;action=mix', 'mix color'); insert into iam_group_role (role_id, principal_id) values ('r_op_rc__art', 'g___oc-group'), -- color ('r_pp_bc__mix', 'g___cb-group'), -- color - ('r_pp_rc__mix', 'g___cr-group'); -- color + ('r_pp_rc__mix', 'g___cr-group'), -- color + ('r_pp_gc__mix', 'g___cg-group'); -- color insert into iam_user_role (role_id, principal_id) @@ -125,18 +134,21 @@ begin; ('apm___colors', 'apa____clare', 'clare'), ('apm___colors', 'apa____cindy', 'cindy'), ('apm___colors', 'apa____carly', 'carly'), + ('apm___colors', 'apa_____cora', 'cora'), ('apm___colors', 'apa____ciara', 'ciara'); update auth_account set iam_user_id = 'u______clare' where public_id = 'apa____clare'; update auth_account set iam_user_id = 'u______cindy' where public_id = 'apa____cindy'; update auth_account set iam_user_id = 'u______carly' where public_id = 'apa____carly'; + update auth_account set iam_user_id = 'u_______cora' where public_id = 'apa_____cora'; update auth_account set iam_user_id = 'u______ciara' where public_id = 'apa____ciara'; insert into static_host_catalog (project_id, public_id, name) values ('p____bcolors', 'c___cb-sthcl', 'Blue Color Static Catalog'), - ('p____rcolors', 'c___cr-sthcl', 'Red Color Static Catalog'); + ('p____rcolors', 'c___cr-sthcl', 'Red Color Static Catalog'), + ('p____gcolors', 'c___cg-sthcl', 'Green Color Static Catalog'); insert into auth_token (key_id, auth_account_id, public_id, token) @@ -144,7 +156,8 @@ begin; ('kdkv_______colors', 'apa____clare', 'tok____clare', 'tok____clare'::bytea), ('kdkv_______colors', 'apa____cindy', 'tok____cindy', 'tok____cindy'::bytea), ('kdkv_______colors', 'apa____ciara', 'tok____ciara', 'tok____ciara'::bytea), - ('kdkv_______colors', 'apa____carly', 'tok____carly', 'tok____carly'::bytea); + ('kdkv_______colors', 'apa____carly', 'tok____carly', 'tok____carly'::bytea), + ('kdkv_______colors', 'apa_____cora', 'tok_____cora', 'tok_____cora'::bytea); insert into static_host (catalog_id, public_id, address) @@ -167,7 +180,17 @@ begin; ('c___cr-sthcl', 'h_____cr__06', '6.red.color'), ('c___cr-sthcl', 'h_____cr__07', '7.red.color'), ('c___cr-sthcl', 'h_____cr__08', '8.red.color'), - ('c___cr-sthcl', 'h_____cr__09', '9.red.color'); + ('c___cr-sthcl', 'h_____cr__09', '9.red.color'), + + ('c___cg-sthcl', 'h_____cg__01', '1.green.color'), + ('c___cg-sthcl', 'h_____cg__02', '2.green.color'), + ('c___cg-sthcl', 'h_____cg__03', '3.green.color'), + ('c___cg-sthcl', 'h_____cg__04', '4.green.color'), + ('c___cg-sthcl', 'h_____cg__05', '5.green.color'), + ('c___cg-sthcl', 'h_____cg__06', '6.green.color'), + ('c___cg-sthcl', 'h_____cg__07', '7.green.color'), + ('c___cg-sthcl', 'h_____cg__08', '8.green.color'), + ('c___cg-sthcl', 'h_____cg__09', '9.green.color'); insert into host_dns_name (host_id, name) @@ -190,7 +213,17 @@ begin; ('h_____cr__06', '6.red.color'), ('h_____cr__07', '7.red.color'), ('h_____cr__08', '8.red.color'), - ('h_____cr__09', '9.red.color'); + ('h_____cr__09', '9.red.color'), + + ('h_____cg__01', '1.green.color'), + ('h_____cg__02', '2.green.color'), + ('h_____cg__03', '3.green.color'), + ('h_____cg__04', '4.green.color'), + ('h_____cg__05', '5.green.color'), + ('h_____cg__06', '6.green.color'), + ('h_____cg__07', '7.green.color'), + ('h_____cg__08', '8.green.color'), + ('h_____cg__09', '9.green.color'); insert into host_ip_address (host_id, address) @@ -213,7 +246,17 @@ begin; ('h_____cr__06', '2001:4860:4860::6666'), ('h_____cr__07', '77.77.77.77'), ('h_____cr__08', '2001:4860:4860::8888'), - ('h_____cr__09', '99.99.99.99'); + ('h_____cr__09', '99.99.99.99'), + + ('h_____cg__01', '111.111.111.111'), + ('h_____cg__02', '3001:5860:5860::3333'), + ('h_____cg__03', '112.112.112.112'), + ('h_____cg__04', '3001:5860:5860::5555'), + ('h_____cg__05', '113.113.113.113'), + ('h_____cg__06', '3001:5860:5860::7777'), + ('h_____cg__07', '114.114.114.114'), + ('h_____cg__08', '3001:5860:5860::9999'), + ('h_____cg__09', '115.115.115.115'); insert into static_host_set (catalog_id, public_id, name) @@ -221,7 +264,9 @@ begin; ('c___cb-sthcl', 's___1cb-sths', 'Blue Color Static Set 1'), ('c___cb-sthcl', 's___2cb-sths', 'Blue Color Static Set 2'), ('c___cr-sthcl', 's___1cr-sths', 'Red Color Static Set 1'), - ('c___cr-sthcl', 's___2cr-sths', 'Red Color Static Set 2'); + ('c___cr-sthcl', 's___2cr-sths', 'Red Color Static Set 2'), + ('c___cg-sthcl', 's___1cg-sths', 'Green Color Static Set 1'), + ('c___cg-sthcl', 's___2cg-sths', 'Green Color Static Set 2'); insert into static_host_set_member @@ -235,7 +280,8 @@ begin; (project_id, public_id, name) values ('p____bcolors', 't_________cb', 'Blue Color Target'), - ('p____rcolors', 't_________cr', 'Red Color Target'); + ('p____rcolors', 't_________cr', 'Red Color Target'), + ('p____gcolors', 't_________cg', 'Green Color Target'); insert into target_host_set (project_id, target_id, host_set_id) @@ -245,6 +291,11 @@ begin; ('p____rcolors', 't_________cr', 's___1cr-sths'), ('p____rcolors', 't_________cr', 's___2cr-sths'); + insert into target_address + (target_id, address) + values + ('t_________cg', '8.8.8.8'); + insert into credential_vault_store (project_id, public_id, name, description, vault_address, namespace) values @@ -263,12 +314,26 @@ begin; ('p____bcolors', 't_________cb', 'vl______cvl', 'brokered'); insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bcolors', 't_________cb', 's___1cb-sths', 'h_____cb__01', 'u______clare', 'tok____clare', 'abc'::bytea, 'ep1', 's1_____clare'), - ('p____bcolors', 't_________cb', 's___1cb-sths', 'h_____cb__01', 'u______cindy', 'tok____cindy', 'abc'::bytea, 'ep1', 's1_____cindy'), - ('p____bcolors', 't_________cb', 's___1cb-sths', 'h_____cb__01', 'u______cindy', 'tok____cindy', 'abc'::bytea, 'ep1', 's1_____ciara'), - ('p____bcolors', 't_________cb', 's___1cb-sths', 'h_____cb__01', 'u______carly', 'tok____carly', 'abc'::bytea, 'ep1', 's1_____carly'); + ('p____bcolors', 't_________cb', 'u______clare', 'tok____clare', 'abc'::bytea, 'ep1', 's1_____clare'), + ('p____bcolors', 't_________cb', 'u______cindy', 'tok____cindy', 'abc'::bytea, 'ep1', 's1_____cindy'), + ('p____bcolors', 't_________cb', 'u______cindy', 'tok____cindy', 'abc'::bytea, 'ep1', 's1_____ciara'), + ('p____bcolors', 't_________cb', 'u______carly', 'tok____carly', 'abc'::bytea, 'ep1', 's1_____carly'), + ('p____gcolors', 't_________cg', 'u_______cora', 'tok_____cora', 'abc'::bytea, 'ep1', 's1______cora'); + + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1_____clare', 's___1cb-sths', 'h_____cb__01'), + ('s1_____cindy', 's___1cb-sths', 'h_____cb__01'), + ('s1_____ciara', 's___1cb-sths', 'h_____cb__01'), + ('s1_____carly', 's___1cb-sths', 'h_____cb__01'); + + insert into session_target_address + (session_id, target_id) + values + ('s1______cora', 't_________cg'); insert into session_connection (session_id, public_id) diff --git a/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql b/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql index 602dadd7b..c1de08227 100644 --- a/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql +++ b/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql @@ -122,8 +122,13 @@ begin; ('r_pp___tacos', 'u________bob'); insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p______tacos', 't__________t', 's____1t-sths', 'h______t__01', 'u________jim', 'tok______jim', 'abc'::bytea, 'ep1', 's1_______jim'); + ('p______tacos', 't__________t', 'u________jim', 'tok______jim', 'abc'::bytea, 'ep1', 's1_______jim'); + + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1_______jim', 's____1t-sths', 'h______t__01'); commit; diff --git a/internal/db/sqltest/tests/session/delete_session_auth_token.sql b/internal/db/sqltest/tests/session/delete_session_auth_token.sql new file mode 100644 index 000000000..ce9ebd325 --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_auth_token.sql @@ -0,0 +1,28 @@ +begin; + select plan(12); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have a session for both auth token + select is(count(*), 1::bigint) from session where auth_token_id = 'tok____clare'; + select is(count(*), 1::bigint) from session where auth_token_id = 'tok____carly'; + + -- Delete auth tokens, expect no errors + delete from auth_token where public_id = 'tok____clare' or public_id = 'tok____carly'; + select is(count(*), 0::bigint) from auth_token where public_id = 'tok____clare'; + select is(count(*), 0::bigint) from auth_token where public_id = 'tok____carly'; + + -- Ensure we no longer have sessions associated with auth tokens + select is(count(*), 0::bigint) from session where auth_token_id = 'tok____clare'; + select is(count(*), 0::bigint) from session where auth_token_id = 'tok____carly'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____clare'; + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____carly'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/session/delete_session_host.sql b/internal/db/sqltest/tests/session/delete_session_host.sql new file mode 100644 index 000000000..d2b26fa5a --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_host.sql @@ -0,0 +1,25 @@ +begin; + select plan(9); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have 4 sessions using this host + select is(count(*), 4::bigint) from session_host_set_host where host_id = 'h_____cb__01'; + + -- Delete host, expect no errors + delete from host where public_id = 'h_____cb__01'; + select is(count(*), 0::bigint) from host where public_id = 'h_____cb__01'; + + -- Ensure we no longer have sessions associated with this host + select is(count(*), 0::bigint) from session_host_set_host where host_id = 'h_____cb__01'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____clare'; + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____carly'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/session/delete_session_host_set.sql b/internal/db/sqltest/tests/session/delete_session_host_set.sql new file mode 100644 index 000000000..89711b898 --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_host_set.sql @@ -0,0 +1,25 @@ +begin; + select plan(9); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have 4 sessions using this host set + select is(count(*), 4::bigint) from session_host_set_host where host_set_id = 's___1cb-sths'; + + -- Delete host set, expect no errors + delete from host_set where public_id = 's___1cb-sths'; + select is(count(*), 0::bigint) from host_set where public_id = 's___1cb-sths'; + + -- Ensure we no longer have sessions associated with this host set + select is(count(*), 0::bigint) from session_host_set_host where host_set_id = 's___1cb-sths'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____clare'; + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____carly'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/session/delete_session_project.sql b/internal/db/sqltest/tests/session/delete_session_project.sql new file mode 100644 index 000000000..7a3be7242 --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_project.sql @@ -0,0 +1,25 @@ +begin; + select plan(9); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have 4 sessions using this project + select is(count(*), 4::bigint) from session where project_id = 'p____bcolors'; + + -- Delete project, expect no errors + delete from iam_scope_project where scope_id = 'p____bcolors'; + select is(count(*), 0::bigint) from iam_scope_project where scope_id = 'p____bcolors'; + + -- Ensure we no longer have sessions associated with this project + select is(count(*), 0::bigint) from session where project_id = 'p____bcolors'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____clare'; + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____carly'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/session/delete_session_target_address.sql b/internal/db/sqltest/tests/session/delete_session_target_address.sql new file mode 100644 index 000000000..d0d9c1483 --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_target_address.sql @@ -0,0 +1,25 @@ +begin; + select plan(9); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1______cora' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have a session using this target address + select is(count(*), 1::bigint) from session_target_address where target_id = 't_________cg'; + + -- Delete target address, expect no errors + delete from target_address where target_id = 't_________cg'; + select is(count(*), 0::bigint) from target_address where target_id = 't_________cg'; + + -- Ensure we no longer have sessions associated with this target address + select is(count(*), 0::bigint) from session_target_address where target_id = 't_________cg'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1______cora'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/session/delete_session_user.sql b/internal/db/sqltest/tests/session/delete_session_user.sql new file mode 100644 index 000000000..7033acf31 --- /dev/null +++ b/internal/db/sqltest/tests/session/delete_session_user.sql @@ -0,0 +1,28 @@ +begin; + select plan(12); + + -- Ensure session state table is populated + select is(count(*), 1::bigint) from session_state where session_id = 's1_____clare' and state='pending'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____cindy' and state='terminated'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____ciara' and state='canceling'; + select is(count(*), 1::bigint) from session_state where session_id = 's1_____carly' and state='active'; + + -- Check that we have a session for both users + select is(count(*), 1::bigint) from session where user_id = 'u______clare'; + select is(count(*), 1::bigint) from session where user_id = 'u______carly'; + + -- Delete users, expect no errors + delete from iam_user where public_id = 'u______clare' or public_id = 'u______carly'; + select is(count(*), 0::bigint) from iam_user where public_id = 'u______clare'; + select is(count(*), 0::bigint) from iam_user where public_id = 'u______carly'; + + -- Ensure we no longer have sessions associated with the users + select is(count(*), 0::bigint) from session where user_id = 'u______clare'; + select is(count(*), 0::bigint) from session where user_id = 'u______carly'; + + -- Ensure sessions that were pending or active are now in canceling state + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____clare'; + select is(count(*), 1::bigint) from session_state where state = 'canceling' and session_id = 's1_____carly'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql index a4c80b70d..a92ec3232 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new credentials dimension insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -25,9 +29,13 @@ begin; -- * same host -- should not result in a new credential dimension insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -39,9 +47,13 @@ begin; -- start another session, should result in a new credential dimension insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's3____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's3____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s3____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -50,9 +62,13 @@ begin; -- start another session, should result in a one new credential dimensions insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's4____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's4____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s4____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -65,9 +81,13 @@ begin; -- start another session, should result in a one new credential dimensions since one changed insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's5____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's5____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s5____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -80,9 +100,13 @@ begin; -- then test creating a session delete from credential_vault_library; insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's6____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's6____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s6____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; select is(credential_group_key, 'no credentials') from wh_session_accumulating_fact where session_id = 's6____walter'; insert into session_connection @@ -93,9 +117,13 @@ begin; -- insert into a session for a target that never had any credentials associated with it. insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________ws', 's___1ws-sths', 'h_____ws__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's7____walter'); + ('p____bwidget', 't_________ws', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's7____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s7____walter', 's___1ws-sths', 'h_____ws__01'); select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; select is(credential_group_key, 'no credentials') from wh_session_accumulating_fact where session_id = 's7____walter'; insert into session_connection diff --git a/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql b/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql index 32fca8ded..daa66576b 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql index cef7e3af9..f0f00201d 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql @@ -14,9 +14,13 @@ begin; -- insert session and session_credential_dynamic, should result in a three new credential dimensions insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values @@ -29,9 +33,13 @@ begin; update credential_vault_library set vault_path = '/secrets/tcp/user' where public_id = 'vl______wvl2'; insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql index 86ca35f71..91e728474 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql @@ -4,9 +4,13 @@ begin; select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/wh/host_dimension/session_existing_host_network.sql b/internal/db/sqltest/tests/wh/host_dimension/session_existing_host_network.sql new file mode 100644 index 000000000..a406be826 --- /dev/null +++ b/internal/db/sqltest/tests/wh/host_dimension/session_existing_host_network.sql @@ -0,0 +1,78 @@ +begin; + select plan(8); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- create network address dimension using static host + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); + + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + select is(count(*), 1::bigint) from wh_network_address_dimension + where address = '1.big.widget' + and address_type = 'DNS Name' + and ip_address_family = 'Not Applicable' + and private_ip_address_indicator = 'Not Applicable' + and dns_name = '1.big.widget' + and ip4_address = 'Not Applicable' + and ip6_address = 'Not Applicable'; + select is(count(*), 1::bigint) + from wh_network_address_group_membership g + left join wh_host_dimension h on g.network_address_group_key = h.network_address_group_key + where g.network_address = '1.big.widget' + and h.target_id = 't_________wb' + and h.host_id = 'h_____wb__01' + and h.host_set_id = 's___1wb-sths' + and h.current_row_indicator = 'Current'; + + -- delete target host source associations to enforce mutually exclusive relationship to a target address + delete from target_host_set where target_id = 't_________wb'; + + -- update target to use the same address, but as a direct network address association + insert into target_address + (target_id, address) + values + ('t_________wb', '1.big.widget'); + select is(count(*), 0::bigint) from target_host_set where target_id = 't_________wb'; + + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + insert into session_target_address + (session_id, target_id) + values + ('s2____walter', 't_________wb'); + + -- this should have expired the old host_dimension that had values inserted in the columns related to hosts, host sets, & host catalogs. + -- this should have created a new host_dimension for a static address with 'Not Applicable' values set for columns related to hosts, host sets, & host catalogs. + -- this should have used the existing network_address_dimension created from the session using a host source + select is(count(*), 2::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + select is(count(*), 1::bigint) from wh_network_address_dimension + where address = '1.big.widget' + and address_type = 'DNS Name' + and ip_address_family = 'Not Applicable' + and private_ip_address_indicator = 'Not Applicable' + and dns_name = '1.big.widget' + and ip4_address = 'Not Applicable' + and ip6_address = 'Not Applicable'; + select is(count(*), 1::bigint) + from wh_network_address_group_membership g + left join wh_host_dimension h on g.network_address_group_key = h.network_address_group_key + where g.network_address = '1.big.widget' + and h.target_id = 't_________wb' + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.current_row_indicator = 'Current'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/wh/host_dimension/session_multiple_sessions.sql b/internal/db/sqltest/tests/wh/host_dimension/session_multiple_sessions.sql index c992fa9f3..06e413787 100644 --- a/internal/db/sqltest/tests/wh/host_dimension/session_multiple_sessions.sql +++ b/internal/db/sqltest/tests/wh/host_dimension/session_multiple_sessions.sql @@ -10,10 +10,13 @@ begin; -- insert first session, should result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__02-plgh' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); - + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-plghs', 'h_____wb__02-plgh'); select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; -- another session with: @@ -22,10 +25,14 @@ begin; -- * same host -- should not result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__02-plgh' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); - + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1wb-plghs', 'h_____wb__02-plgh'); + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; -- another session with: @@ -34,9 +41,13 @@ begin; -- * same host -- should not result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__02-plgh' , 'u_____warren' , 'tok___warren' , 'abc'::bytea , 'ep1' , 's3____walter'); + ('p____bwidget' , 't_________wb' , 'u_____warren' , 'tok___warren' , 'abc'::bytea , 'ep1' , 's3____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s3____walter', 's___1wb-plghs', 'h_____wb__02-plgh'); select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; @@ -52,9 +63,13 @@ begin; -- should result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__02-plgh' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's4____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's4____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s4____walter', 's___1wb-plghs', 'h_____wb__02-plgh'); select is(count(*), 2::bigint) from wh_host_dimension where organization_id = 'o_____widget'; @@ -70,9 +85,13 @@ begin; -- should result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__02-plgh' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's5____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's5____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s5____walter', 's___1wb-plghs', 'h_____wb__02-plgh'); select is(count(*), 3::bigint) from wh_host_dimension where organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/host_dimension/session_target_address.sql b/internal/db/sqltest/tests/wh/host_dimension/session_target_address.sql new file mode 100644 index 000000000..ee69917c6 --- /dev/null +++ b/internal/db/sqltest/tests/wh/host_dimension/session_target_address.sql @@ -0,0 +1,138 @@ +begin; + select plan(12); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- setup target to use a ipv4 static address + insert into target_address + (target_id, address) + values + ('t_________wb', '8.6.4.2'); + + -- insert first session, should result in a new host dimension + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_target_address + (session_id, target_id) + values + ('s1____walter', 't_________wb'); + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- expected wh_network_address_dimension row is created for ipv4 + select is(count(*), 1::bigint) from wh_network_address_dimension + where address = '8.6.4.2' + and address_type = 'IP Address' + and ip_address_family = 'IPv4' + and private_ip_address_indicator = 'Public IP address' + and dns_name = 'Not Applicable' + and ip4_address = '8.6.4.2' + and ip6_address = 'Not Applicable'; + + -- expected wh_network_address_group_membership has one row: '8.6.4.2' + select is(count(*), 1::bigint) + from wh_network_address_group_membership gm, + wh_host_dimension h + where h.network_address_group_key = gm.network_address_group_key + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.host_catalog_id = 'Not Applicable' + and h.target_id = 't_________wb'; + + -- update target address association to ipv6 + update target_address set address = 'fe60::6667:5556:4445:3334' where target_id = 't_________wb'; + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + insert into session_target_address + (session_id, target_id) + values + ('s2____walter', 't_________wb'); + + -- should not result in a new host dimension + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- expected wh_network_address_dimension row is created for ipv6 + select is(count(*), 1::bigint) from wh_network_address_dimension + where address = 'fe60::6667:5556:4445:3334' + and address_type = 'IP Address' + and ip_address_family = 'IPv6' + and private_ip_address_indicator = 'Public IP address' + and dns_name = 'Not Applicable' + and ip4_address = 'Not Applicable' + and ip6_address = 'fe60::6667:5556:4445:3334'; + + -- expected wh_network_address_group_membership has two rows: '8.6.4.2' & 'fe60::6667:5556:4445:3334' + select is(count(*), 2::bigint) + from wh_network_address_group_membership gm, + wh_host_dimension h + where h.network_address_group_key = gm.network_address_group_key + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.host_catalog_id = 'Not Applicable' + and h.target_id = 't_________wb'; + + -- update static address association to dns name + update target_address set address = '0.blue.green' where target_id = 't_________wb'; + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's3____walter'); + insert into session_target_address + (session_id, target_id) + values + ('s3____walter', 't_________wb'); + + -- should not result in a new host dimension + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- expected wh_network_address_dimension row is created for dns + select is(count(*), 1::bigint) from wh_network_address_dimension + where address = '0.blue.green' + and address_type = 'DNS Name' + and ip_address_family = 'Not Applicable' + and private_ip_address_indicator = 'Not Applicable' + and dns_name = '0.blue.green' + and ip4_address = 'Not Applicable' + and ip6_address = 'Not Applicable'; + + -- expected wh_network_address_group_membership has three rows: '8.6.4.2' & 'fe60::6667:5556:4445:3334' & '0.blue.green' + select is(count(*), 3::bigint) + from wh_network_address_group_membership gm, + wh_host_dimension h + where h.network_address_group_key = gm.network_address_group_key + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.host_catalog_id = 'Not Applicable' + and h.target_id = 't_________wb'; + + -- create second session with the same resources + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's4____walter'); + insert into session_target_address + (session_id, target_id) + values + ('s4____walter', 't_________wb'); + + -- should not result in a new host dimension + select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; + + -- expected wh_network_address_group_membership does not change. + select is(count(*), 3::bigint) + from wh_network_address_group_membership gm, + wh_host_dimension h + where h.network_address_group_key = gm.network_address_group_key + and h.host_id = 'Not Applicable' + and h.host_set_id = 'Not Applicable' + and h.host_catalog_id = 'Not Applicable' + and h.target_id = 't_________wb'; + + select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/wh/host_dimension/session_update.sql b/internal/db/sqltest/tests/wh/host_dimension/session_update.sql index 8b09cffd8..81f3c1107 100644 --- a/internal/db/sqltest/tests/wh/host_dimension/session_update.sql +++ b/internal/db/sqltest/tests/wh/host_dimension/session_update.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new host dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-plghs' , 'h_____wb__01-plgh' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-plghs', 'h_____wb__01-plgh'); select is(count(*), 1::bigint) from wh_host_dimension where organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/session/insert.sql b/internal/db/sqltest/tests/wh/session/insert.sql index 28cd40ec6..fa0ebc822 100644 --- a/internal/db/sqltest/tests/wh/session/insert.sql +++ b/internal/db/sqltest/tests/wh/session/insert.sql @@ -9,9 +9,13 @@ begin; -- insert first session, should result in a new wh_session_accumulating_fact insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_session_accumulating_fact where session_id = 's1____walter'; diff --git a/internal/db/sqltest/tests/wh/session_connection/insert.sql b/internal/db/sqltest/tests/wh/session_connection/insert.sql index 4f39ab90c..34f066250 100644 --- a/internal/db/sqltest/tests/wh/session_connection/insert.sql +++ b/internal/db/sqltest/tests/wh/session_connection/insert.sql @@ -8,10 +8,13 @@ begin; select is(count(*), 0::bigint) from wh_session_connection_accumulating_fact where connection_id = 'sc1____walter'; insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); - + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_connection (session_id, public_id) values diff --git a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql index ca850327c..f3ff41ee3 100644 --- a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql +++ b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql @@ -10,10 +10,13 @@ begin; --- insert single credential insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); - + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql index d714925ec..d7db3721e 100644 --- a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql +++ b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql @@ -10,9 +10,13 @@ begin; --- multiple single credentials insert into session - ( project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + ('p____bwidget', 't_________wb', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); insert into session_credential_dynamic ( session_id, library_id, credential_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/wh/user_dimension/oidc_auth_new_session.sql b/internal/db/sqltest/tests/wh/user_dimension/oidc_auth_new_session.sql index a5b86cb30..b2c996493 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/oidc_auth_new_session.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/oidc_auth_new_session.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'oidc__walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'oidc__walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter'; select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter'; @@ -41,9 +45,13 @@ begin; -- insert session without full name or email insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____warren' , 'oidc__warren' , 'abc'::bytea , 'ep1' , 's1____warren'); + ('p____bwidget' , 't_________wb' , 'u_____warren' , 'oidc__warren' , 'abc'::bytea , 'ep1' , 's1____warren'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____warren', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren'; select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren'; diff --git a/internal/db/sqltest/tests/wh/user_dimension/password_auth_description_change.sql b/internal/db/sqltest/tests/wh/user_dimension/password_auth_description_change.sql index 860eff0cf..225839030 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/password_auth_description_change.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/password_auth_description_change.sql @@ -11,9 +11,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter'; select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter'; @@ -52,9 +56,13 @@ begin; -- * same host -- should result in a new user dimension due to auth change. insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 2::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/user_dimension/password_auth_new_session.sql b/internal/db/sqltest/tests/wh/user_dimension/password_auth_new_session.sql index b6e01283f..1f9b0ef0f 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/password_auth_new_session.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/password_auth_new_session.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter'; select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter'; diff --git a/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions.sql b/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions.sql index 3dbe9c8d3..f4affaa0c 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; @@ -22,9 +26,13 @@ begin; -- * same host -- should not result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; @@ -34,9 +42,13 @@ begin; -- * different host -- should not result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__02' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's3____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's3____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s3____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions_different_auth.sql b/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions_different_auth.sql index 10928629c..80e405a78 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions_different_auth.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/session_multiple_sessions_different_auth.sql @@ -11,9 +11,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; @@ -23,9 +27,13 @@ begin; -- * same host -- should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok1__walter' , 'abc'::bytea , 'ep1' , 's4____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok1__walter' , 'abc'::bytea , 'ep1' , 's4____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s4____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 2::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter' and auth_account_id = 'apa1__walter'; diff --git a/internal/db/sqltest/tests/wh/user_dimension/session_update.sql b/internal/db/sqltest/tests/wh/user_dimension/session_update.sql index 341da6e70..f2c865469 100644 --- a/internal/db/sqltest/tests/wh/user_dimension/session_update.sql +++ b/internal/db/sqltest/tests/wh/user_dimension/session_update.sql @@ -10,9 +10,13 @@ begin; -- insert first session, should result in a new user dimension insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'tok___walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); select is(count(*), 1::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; diff --git a/internal/errors/code.go b/internal/errors/code.go index acb614ef1..25ebf0c23 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -79,6 +79,7 @@ const ( // client and server error codes Unauthorized Code = 401 // Unauthorized represents the operation is unauthorized Forbidden Code = 403 // Forbidden represents the operation is forbidden + Conflict Code = 409 // Conflict represents the operation failed due to failed pre-condition or was aborted. Internal Code = 500 // InternalError represents the system encountered an unexpected condition. // DB errors are reserved Codes from 1000-1999 diff --git a/internal/errors/info.go b/internal/errors/info.go index 8e8cd6d2a..914396fdb 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -152,6 +152,10 @@ var errorCodeInfo = map[Code]Info{ Message: "unauthorized", Kind: Other, }, + Conflict: { + Message: "conflict", + Kind: Integrity, + }, CheckConstraint: { Message: "constraint check failed", Kind: Integrity, diff --git a/internal/errors/is.go b/internal/errors/is.go index 01bc22f22..a2b30c82d 100644 --- a/internal/errors/is.go +++ b/internal/errors/is.go @@ -94,3 +94,13 @@ func IsNotFoundError(err error) bool { return false } + +// IsConflictError returns a boolean indicating whether the error is known to +// report a pre-conditional check violation or an aborted transaction. +func IsConflictError(err error) bool { + if err == nil { + return false + } + + return Match(T(Conflict), err) +} diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index d5fd88fe4..3fd7c4d2b 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -3062,7 +3062,7 @@ }, "/v1/targets/{id}:add-host-sources": { "post": { - "summary": "Adds existing Host Sources to a Target.", + "summary": "Adds existing Host Sources to a Target. Cannot be used on targets that have their address field set.", "operationId": "TargetService_AddTargetHostSources", "responses": { "200": { @@ -3329,7 +3329,7 @@ }, "/v1/targets/{id}:set-host-sources": { "post": { - "summary": "Sources the Host Sources on the Target.", + "summary": "Sources the Host Sources on the Target. Cannot be used on targets that have their address field set.", "operationId": "TargetService_SetTargetHostSources", "responses": { "200": { @@ -5710,6 +5710,10 @@ }, "description": "Output only. The available actions on this resource for this user.", "readOnly": true + }, + "address": { + "type": "string", + "description": "Optional string value that represents a network resource and is used when establishing a session." } }, "title": "Target contains all fields related to a Target resource" diff --git a/internal/gen/controller/api/services/target_service.pb.go b/internal/gen/controller/api/services/target_service.pb.go index 806a597e9..487c77971 100644 --- a/internal/gen/controller/api/services/target_service.pb.go +++ b/internal/gen/controller/api/services/target_service.pb.go @@ -1619,7 +1619,7 @@ var file_controller_api_services_v1_target_service_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x99, 0x14, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x95, 0x15, 0x0a, 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa2, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, @@ -1686,7 +1686,7 @@ var file_controller_api_services_v1_target_service_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, 0x22, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x2d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x01, 0x2a, - 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xe9, 0x01, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x54, 0x61, + 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xa7, 0x02, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, @@ -1695,99 +1695,107 @@ var file_controller_api_services_v1_target_service_proto_rawDesc = []byte{ 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x5e, 0x92, 0x41, 0x29, 0x12, 0x27, 0x41, 0x64, 0x64, 0x73, 0x20, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2c, 0x22, 0x21, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x64, 0x64, 0x2d, 0x68, 0x6f, 0x73, - 0x74, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x12, 0xe9, 0x01, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5e, - 0x92, 0x41, 0x29, 0x12, 0x27, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x2c, 0x22, 0x21, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, - 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xf3, - 0x01, 0x0a, 0x17, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, - 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x73, 0x65, 0x22, 0x9b, 0x01, 0x92, 0x41, 0x66, 0x12, 0x64, 0x41, 0x64, 0x64, 0x73, 0x20, 0x65, + 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x2e, 0x20, 0x43, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x75, 0x73, 0x65, 0x64, + 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, + 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x74, 0x68, 0x65, 0x69, 0x72, 0x20, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x73, 0x65, 0x74, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x2c, 0x22, 0x21, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x64, 0x64, 0x2d, 0x68, 0x6f, 0x73, 0x74, 0x2d, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x12, 0xa7, 0x02, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, + 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9b, 0x01, 0x92, + 0x41, 0x66, 0x12, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x48, 0x6f, 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x20, 0x43, 0x61, 0x6e, 0x6e, + 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, + 0x74, 0x68, 0x65, 0x69, 0x72, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x20, 0x73, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2c, 0x22, 0x21, + 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xf3, 0x01, 0x0a, 0x17, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x92, 0x41, 0x27, 0x12, 0x25, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, - 0x73, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x66, - 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x2f, 0x22, 0x24, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x68, - 0x6f, 0x73, 0x74, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x12, 0x87, 0x02, 0x0a, 0x1a, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x12, 0x3d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x48, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x6a, 0x92, 0x41, 0x2f, 0x12, 0x2d, 0x41, 0x64, 0x64, 0x73, 0x20, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, - 0x6c, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x27, 0x2f, 0x76, - 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, - 0x64, 0x64, 0x2d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2d, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x84, - 0x02, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3d, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x67, 0x92, 0x41, - 0x2c, 0x12, 0x2a, 0x53, 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x43, 0x72, 0x65, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x6f, - 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x32, 0x22, 0x27, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, - 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x91, 0x02, 0x0a, 0x1d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x5f, 0x92, 0x41, 0x27, 0x12, 0x25, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x73, 0x20, 0x48, 0x6f, + 0x73, 0x74, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x2f, 0x22, 0x24, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, + 0x69, 0x64, 0x7d, 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x68, 0x6f, 0x73, 0x74, 0x2d, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x12, 0x87, 0x02, 0x0a, 0x1a, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, + 0x3d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x54, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6a, + 0x92, 0x41, 0x2f, 0x12, 0x2d, 0x41, 0x64, 0x64, 0x73, 0x20, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x20, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x27, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x64, 0x64, 0x2d, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x84, 0x02, 0x0a, 0x1a, 0x53, + 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6b, 0x92, 0x41, - 0x2d, 0x12, 0x2b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x73, 0x20, 0x43, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x66, 0x72, - 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x35, 0x22, 0x2a, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x42, 0x57, 0x5a, 0x4b, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0xa2, 0xe3, 0x29, 0x06, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x67, 0x92, 0x41, 0x2c, 0x12, 0x2a, 0x53, + 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, + 0x27, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, + 0x7d, 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x12, 0x91, 0x02, 0x0a, 0x1d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6b, 0x92, 0x41, 0x2d, 0x12, 0x2b, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x73, 0x20, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x20, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, + 0x22, 0x2a, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x2d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x62, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x42, 0x57, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0xa2, 0xe3, 0x29, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/gen/controller/api/services/target_service_grpc.pb.go b/internal/gen/controller/api/services/target_service_grpc.pb.go index d4bcaef83..2ee4e350b 100644 --- a/internal/gen/controller/api/services/target_service_grpc.pb.go +++ b/internal/gen/controller/api/services/target_service_grpc.pb.go @@ -53,8 +53,9 @@ type TargetServiceClient interface { // Host Sources added to the provided Target must be a child of a Catalog that // is a child of the same scope as this Target. If the scope or Target IDs are // missing, malformed, or reference non-existing resources, an error is - // returned. An error is returned if a Host Source is attempted to be added to a - // target that is already present on the Target. + // returned. An error is returned if a Host Source is attempted to be added to + // a target that is already present on the Target. If the given target already + // has its address field set, a Bad Request error is returned. AddTargetHostSources(ctx context.Context, in *AddTargetHostSourcesRequest, opts ...grpc.CallOption) (*AddTargetHostSourcesResponse, error) // SetTargetHostSources sets the Target's Host Sources. Any existing Host // Sources on the Target are deleted if they are not included in this request. @@ -62,7 +63,8 @@ type TargetServiceClient interface { // Host Sources will be set. All Host Sources in the request must be a child // of a Catalog that is in the same scope as the provided Target. If any IDs // are missing, malformed, or references a non-existing resource, an error is - // returned. + // returned. If the given target already has its address field set, a Bad + // Request error is returned. SetTargetHostSources(ctx context.Context, in *SetTargetHostSourcesRequest, opts ...grpc.CallOption) (*SetTargetHostSourcesResponse, error) // RemoveTargetHostSources removes the Host Sources from the specified Target. // The provided request must include the Target ID for the Target from which @@ -252,8 +254,9 @@ type TargetServiceServer interface { // Host Sources added to the provided Target must be a child of a Catalog that // is a child of the same scope as this Target. If the scope or Target IDs are // missing, malformed, or reference non-existing resources, an error is - // returned. An error is returned if a Host Source is attempted to be added to a - // target that is already present on the Target. + // returned. An error is returned if a Host Source is attempted to be added to + // a target that is already present on the Target. If the given target already + // has its address field set, a Bad Request error is returned. AddTargetHostSources(context.Context, *AddTargetHostSourcesRequest) (*AddTargetHostSourcesResponse, error) // SetTargetHostSources sets the Target's Host Sources. Any existing Host // Sources on the Target are deleted if they are not included in this request. @@ -261,7 +264,8 @@ type TargetServiceServer interface { // Host Sources will be set. All Host Sources in the request must be a child // of a Catalog that is in the same scope as the provided Target. If any IDs // are missing, malformed, or references a non-existing resource, an error is - // returned. + // returned. If the given target already has its address field set, a Bad + // Request error is returned. SetTargetHostSources(context.Context, *SetTargetHostSourcesRequest) (*SetTargetHostSourcesResponse, error) // RemoveTargetHostSources removes the Host Sources from the specified Target. // The provided request must include the Target ID for the Target from which diff --git a/internal/proto/controller/api/resources/targets/v1/target.proto b/internal/proto/controller/api/resources/targets/v1/target.proto index 2a82822eb..9ced1302e 100644 --- a/internal/proto/controller/api/resources/targets/v1/target.proto +++ b/internal/proto/controller/api/resources/targets/v1/target.proto @@ -211,6 +211,16 @@ message Target { // Output only. The available actions on this resource for this user. repeated string authorized_actions = 300 [json_name = "authorized_actions"]; // @gotags: `class:"public"` + // Optional string value that represents a network resource and is used when establishing a session. + google.protobuf.StringValue address = 540 [ + json_name = "address", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "address" + that: "address" + } + ]; // @gotags: `class:"public"` + // Deprecated fields reserved "application_credential_library_ids", "application_credential_libraries"; reserved 150, 180; diff --git a/internal/proto/controller/api/services/v1/target_service.proto b/internal/proto/controller/api/services/v1/target_service.proto index c4d44fbb8..9ac6a6259 100644 --- a/internal/proto/controller/api/services/v1/target_service.proto +++ b/internal/proto/controller/api/services/v1/target_service.proto @@ -102,8 +102,9 @@ service TargetService { // Host Sources added to the provided Target must be a child of a Catalog that // is a child of the same scope as this Target. If the scope or Target IDs are // missing, malformed, or reference non-existing resources, an error is - // returned. An error is returned if a Host Source is attempted to be added to a - // target that is already present on the Target. + // returned. An error is returned if a Host Source is attempted to be added to + // a target that is already present on the Target. If the given target already + // has its address field set, a Bad Request error is returned. rpc AddTargetHostSources(AddTargetHostSourcesRequest) returns (AddTargetHostSourcesResponse) { option (google.api.http) = { post: "/v1/targets/{id}:add-host-sources" @@ -111,7 +112,7 @@ service TargetService { response_body: "item" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Adds existing Host Sources to a Target." + summary: "Adds existing Host Sources to a Target. Cannot be used on targets that have their address field set." }; } @@ -121,7 +122,8 @@ service TargetService { // Host Sources will be set. All Host Sources in the request must be a child // of a Catalog that is in the same scope as the provided Target. If any IDs // are missing, malformed, or references a non-existing resource, an error is - // returned. + // returned. If the given target already has its address field set, a Bad + // Request error is returned. rpc SetTargetHostSources(SetTargetHostSourcesRequest) returns (SetTargetHostSourcesResponse) { option (google.api.http) = { post: "/v1/targets/{id}:set-host-sources" @@ -129,7 +131,7 @@ service TargetService { response_body: "item" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Sources the Host Sources on the Target." + summary: "Sources the Host Sources on the Target. Cannot be used on targets that have their address field set." }; } diff --git a/internal/proto/controller/custom_options/v1/options.proto b/internal/proto/controller/custom_options/v1/options.proto index 75c419761..428be7fb7 100644 --- a/internal/proto/controller/custom_options/v1/options.proto +++ b/internal/proto/controller/custom_options/v1/options.proto @@ -14,7 +14,8 @@ extend google.protobuf.FieldOptions { MaskMapping mask_mapping = 85464; // generate_sdk_option is a directive used when generating the SDK to - // indicate that an option should be created for the field + // indicate that an option should be created for the field. This also controls + // API options (DefaultX, WithX functions). bool generate_sdk_option = 85412; // subtype is used for oneOf attribute subtypes, to identify that we only need to parse attributes diff --git a/internal/proto/controller/storage/target/store/v1/target.proto b/internal/proto/controller/storage/target/store/v1/target.proto index 374d440bb..4e9f44272 100644 --- a/internal/proto/controller/storage/target/store/v1/target.proto +++ b/internal/proto/controller/storage/target/store/v1/target.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package controller.storage.target.store.v1; +import "controller/custom_options/v1/options.proto"; import "controller/storage/timestamp/v1/timestamp.proto"; option go_package = "github.com/hashicorp/boundary/internal/target/store;store"; @@ -79,6 +80,19 @@ message TargetHostSet { timestamp.v1.Timestamp create_time = 30; } +message TargetAddress { + // target_id of the Target + // @inject_tag: gorm:"primary_key" + string target_id = 10; + + // address associated to the Target + // @inject_tag: `gorm:"not_null"` + string address = 20 [(custom_options.v1.mask_mapping) = { + this: "address" + that: "address" + }]; +} + message CredentialLibrary { // target_id of the Target // @inject_tag: gorm:"primary_key" diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index 87ca1e40e..a1d19cea0 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -34,21 +34,18 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. if newSession.TargetId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id") } - if newSession.HostId == "" { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host id") - } if newSession.UserId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") } - if newSession.HostSetId == "" { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host set id") - } if newSession.AuthTokenId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token id") } if newSession.ProjectId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") } + if newSession.HostId == "" && newSession.HostSetId == "" && newSession.Endpoint == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host source and endpoint") + } if newSession.CtTofuToken != nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "ct is not empty") } @@ -91,6 +88,26 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. return errors.Wrap(ctx, err, op) } + if newSession.HostSetId != "" && newSession.HostId != "" { + hs, err := NewSessionHostSetHost(newSession.PublicId, newSession.HostSetId, newSession.HostId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err = w.Create(ctx, hs); err != nil { + return errors.Wrap(ctx, err, op) + } + returnedSession.HostSetId = hs.HostSetId + returnedSession.HostId = hs.HostId + } else if newSession.Endpoint != "" { + ta, err := NewSessionTargetAddress(newSession.PublicId, newSession.TargetId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err = w.Create(ctx, ta); err != nil { + return errors.Wrap(ctx, err, op) + } + } + for _, cred := range newSession.DynamicCredentials { cred.SessionId = newSession.PublicId } @@ -197,6 +214,13 @@ func (r *Repository) LookupSession(ctx context.Context, sessionId string, opt .. session.StaticCredentials = staticCreds } + sessionHostSetHost := AllocSessionHostSetHost() + if err := read.LookupWhere(ctx, sessionHostSetHost, "session_id = ?", []any{sessionId}); err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op) + } + session.HostSetId = sessionHostSetHost.HostSetId + session.HostId = sessionHostSetHost.HostId + connections, err := fetchConnections(ctx, read, sessionId, db.WithOrder("create_time desc")) if err != nil { return errors.Wrap(ctx, err, op) @@ -583,6 +607,12 @@ func (r *Repository) updateState(ctx context.Context, sessionId string, sessionV if len(returnedStates) < 1 && returnedStates[0].Status != s { return errors.New(ctx, errors.InvalidSessionState, op, fmt.Sprintf("failed to update %s to a state of %s", sessionId, s.String())) } + hostSetHost, err := fetchHostSetHost(ctx, reader, sessionId) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op) + } + updatedSession.HostId = hostSetHost.HostId + updatedSession.HostSetId = hostSetHost.HostSetId return nil }, ) @@ -683,6 +713,15 @@ func fetchConnections(ctx context.Context, r db.Reader, sessionId string, opt .. return connections, nil } +func fetchHostSetHost(ctx context.Context, r db.Reader, sessionId string, opt ...db.Option) (*SessionHostSetHost, error) { + const op = "session.fetchHostSetHost" + var hostSetHost *SessionHostSetHost + if err := r.SearchWhere(ctx, &hostSetHost, "session_id = ?", []any{sessionId}, opt...); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return hostSetHost, nil +} + // decryptAndMaybeUpdateSession switches between the database key and session key based on whether // the session uses a legacy private key or not. It also updates the encrypted session // in the database (if necessary). Eventually we should be able to remove this function diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 73db29fa2..0524d5540 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -307,6 +307,29 @@ func TestRepository_CreateSession(t *testing.T) { }, wantErr: false, }, + { + name: "valid-static-address", + args: args{ + composedOf: TestSessionTargetAddressParams(t, conn, wrapper, iamRepo), + workerAddresses: workerAddresses, + }, + wantErr: false, + }, + { + name: "empty-host-source-endpoint", + args: args{ + composedOf: func() ComposedOf { + c := TestSessionParams(t, conn, wrapper, iamRepo) + c.HostId = "" + c.HostSetId = "" + c.Endpoint = "" + return c + }(), + workerAddresses: workerAddresses, + }, + wantErr: true, + wantIsError: errors.InvalidParameter, + }, { name: "empty-userId", args: args{ @@ -320,19 +343,6 @@ func TestRepository_CreateSession(t *testing.T) { wantErr: true, wantIsError: errors.InvalidParameter, }, - { - name: "empty-hostId", - args: args{ - composedOf: func() ComposedOf { - c := TestSessionParams(t, conn, wrapper, iamRepo) - c.HostId = "" - return c - }(), - workerAddresses: workerAddresses, - }, - wantErr: true, - wantIsError: errors.InvalidParameter, - }, { name: "empty-targetId", args: args{ @@ -346,19 +356,6 @@ func TestRepository_CreateSession(t *testing.T) { wantErr: true, wantIsError: errors.InvalidParameter, }, - { - name: "empty-hostSetId", - args: args{ - composedOf: func() ComposedOf { - c := TestSessionParams(t, conn, wrapper, iamRepo) - c.HostSetId = "" - return c - }(), - workerAddresses: workerAddresses, - }, - wantErr: true, - wantIsError: errors.InvalidParameter, - }, { name: "empty-authTokenId", args: args{ @@ -440,7 +437,7 @@ func TestRepository_CreateSession(t *testing.T) { HostSetId: tt.args.composedOf.HostSetId, AuthTokenId: tt.args.composedOf.AuthTokenId, ProjectId: tt.args.composedOf.ProjectId, - Endpoint: "tcp://127.0.0.1:22", + Endpoint: tt.args.composedOf.Endpoint, ExpirationTime: tt.args.composedOf.ExpirationTime, ConnectionLimit: tt.args.composedOf.ConnectionLimit, DynamicCredentials: tt.args.composedOf.DynamicCredentials, @@ -1254,19 +1251,19 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { name: "canceled-only-once", cancelFk: func() cancelFk { s := setupFn() - var err error - s, err = repo.CancelSession(context.Background(), s.PublicId, s.Version) - require.NoError(t, err) - require.Equal(t, StatusCanceling, s.States[0].Status) - - t := &static.Host{ + h := &static.Host{ Host: &staticStore.Host{ PublicId: s.HostId, }, } + + var err error + s, err = repo.CancelSession(context.Background(), s.PublicId, s.Version) + require.NoError(t, err) + require.Equal(t, StatusCanceling, s.States[0].Status) return cancelFk{ s: s, - fkType: t, + fkType: h, } }(), }, diff --git a/internal/session/session.go b/internal/session/session.go index 3a89ecd18..25de9bf82 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -68,12 +68,8 @@ type Session struct { PublicId string `json:"public_id,omitempty" gorm:"primary_key"` // UserId for the session UserId string `json:"user_id,omitempty" gorm:"default:null"` - // HostId of the session - HostId string `json:"host_id,omitempty" gorm:"default:null"` // TargetId for the session TargetId string `json:"target_id,omitempty" gorm:"default:null"` - // HostSetId for the session - HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` // AuthTokenId for the session AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` // ProjectId for the session @@ -124,6 +120,12 @@ type Session struct { // StaticCredentials for the session. StaticCredentials []*StaticCredential `gorm:"-"` + // HostSetId for the session + HostSetId string `gorm:"-"` + + // HostId of the session + HostId string `gorm:"-"` + // Connections for the session are for read only and are ignored during write operations Connections []*Connection `gorm:"-"` @@ -347,15 +349,9 @@ func (s *Session) validateNewSession() error { if s.UserId == "" { return errors.NewDeprecated(errors.InvalidParameter, op, "missing user id") } - if s.HostId == "" { - return errors.NewDeprecated(errors.InvalidParameter, op, "missing host id") - } if s.TargetId == "" { return errors.NewDeprecated(errors.InvalidParameter, op, "missing target id") } - if s.HostSetId == "" { - return errors.NewDeprecated(errors.InvalidParameter, op, "missing host set id") - } if s.AuthTokenId == "" { return errors.NewDeprecated(errors.InvalidParameter, op, "missing auth token id") } diff --git a/internal/session/session_host_set_host.go b/internal/session/session_host_set_host.go new file mode 100644 index 000000000..3f82d9e2e --- /dev/null +++ b/internal/session/session_host_set_host.go @@ -0,0 +1,69 @@ +package session + +import "github.com/hashicorp/boundary/internal/errors" + +const ( + defaultSessionHostSetHostTableName = "session_host_set_host" +) + +// SessionHostSetHost contains information about a user's session with a target that has a host source association. +type SessionHostSetHost struct { + // SessionId of the session + SessionId string `json:"session_id,omitempty" gorm:"primary_key"` + // HostSetId of the session + HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` + // HostId of the session + HostId string `json:"host_id,omitempty" gorm:"default:null"` + + tableName string `gorm:"-"` +} + +// NewSessionHostSetHost creates a new in memory session to host set & host association. +func NewSessionHostSetHost(sessionId, hostSetId, hostId string) (*SessionHostSetHost, error) { + const op = "session.NewSessionHostSetHost" + if sessionId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing session id") + } + if hostSetId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing host set id") + } + if hostId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing host id") + } + shs := &SessionHostSetHost{ + SessionId: sessionId, + HostSetId: hostSetId, + HostId: hostId, + } + return shs, nil +} + +// TableName returns the tablename to override the default gorm table name +func (s *SessionHostSetHost) TableName() string { + if s.tableName != "" { + return s.tableName + } + return defaultSessionHostSetHostTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (s *SessionHostSetHost) SetTableName(n string) { + s.tableName = n +} + +// AllocSessionHostSet will allocate a SessionHostSetHost +func AllocSessionHostSetHost() *SessionHostSetHost { + return &SessionHostSetHost{} +} + +// Clone creates a clone of the SessionHostSetHost +func (s *SessionHostSetHost) Clone() any { + clone := &SessionHostSetHost{ + SessionId: s.SessionId, + HostSetId: s.HostSetId, + HostId: s.HostId, + } + return clone +} diff --git a/internal/session/session_target_address.go b/internal/session/session_target_address.go new file mode 100644 index 000000000..83e290ce1 --- /dev/null +++ b/internal/session/session_target_address.go @@ -0,0 +1,62 @@ +package session + +import "github.com/hashicorp/boundary/internal/errors" + +const ( + defaultSessionTargetAddressTableName = "session_target_address" +) + +// SessionTargetAddress contains information about a user's session with a target that has a direct network address association. +type SessionTargetAddress struct { + // SessionId of the session + SessionId string `json:"session_id,omitempty" gorm:"primary_key"` + // TargetId of the session + TargetId string `json:"target_id,omitempty" gorm:"default:null"` + + tableName string `gorm:"-"` +} + +// NewSessionTargetAddress creates a new in memory session target address. +func NewSessionTargetAddress(sessionId, targetId string) (*SessionTargetAddress, error) { + const op = "sesssion.NewSessionTargetAddress" + if sessionId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing session id") + } + if targetId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing target id") + } + sta := &SessionTargetAddress{ + SessionId: sessionId, + TargetId: targetId, + } + return sta, nil +} + +// TableName returns the tablename to override the default gorm table name +func (s *SessionTargetAddress) TableName() string { + if s.tableName != "" { + return s.tableName + } + return defaultSessionTargetAddressTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (s *SessionTargetAddress) SetTableName(n string) { + s.tableName = n +} + +// AllocSessionTargetAddress will allocate a SessionTargetAddress +func AllocSessionTargetAddress() *SessionTargetAddress { + return &SessionTargetAddress{} +} + +// Clone creates a clone of the SessionTargetAddress +func (s *SessionTargetAddress) Clone() any { + clone := &SessionTargetAddress{ + SessionId: s.SessionId, + TargetId: s.TargetId, + } + return clone +} diff --git a/internal/session/session_test.go b/internal/session/session_test.go index d90fba65a..1f650e9da 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -45,7 +45,7 @@ func TestSession_Create(t *testing.T) { wantCreateErr bool }{ { - name: "valid", + name: "valid-hostset-host", args: args{ composedOf: composedOf, opt: []Option{WithExpirationTime(exp)}, @@ -66,6 +66,48 @@ func TestSession_Create(t *testing.T) { }, create: true, }, + { + name: "valid-target-address", + args: args{ + composedOf: func() ComposedOf { + c := composedOf + c.HostSetId = "" + c.HostId = "" + return c + }(), + opt: []Option{WithExpirationTime(exp)}, + addresses: defaultAddresses, + }, + want: &Session{ + UserId: composedOf.UserId, + HostId: "", + HostSetId: "", + TargetId: composedOf.TargetId, + AuthTokenId: composedOf.AuthTokenId, + ProjectId: composedOf.ProjectId, + Endpoint: "tcp://127.0.0.1:22", + ExpirationTime: composedOf.ExpirationTime, + ConnectionLimit: composedOf.ConnectionLimit, + DynamicCredentials: composedOf.DynamicCredentials, + StaticCredentials: composedOf.StaticCredentials, + }, + create: true, + }, + { + name: "invalid-missing-target-address-host-source", + args: args{ + composedOf: func() ComposedOf { + c := composedOf + c.HostSetId = "" + c.HostId = "" + c.Endpoint = "" + return c + }(), + addresses: defaultAddresses, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, { name: "empty-userId", args: args{ @@ -79,19 +121,6 @@ func TestSession_Create(t *testing.T) { wantErr: true, wantIsErr: errors.InvalidParameter, }, - { - name: "empty-hostId", - args: args{ - composedOf: func() ComposedOf { - c := composedOf - c.HostId = "" - return c - }(), - addresses: defaultAddresses, - }, - wantErr: true, - wantIsErr: errors.InvalidParameter, - }, { name: "empty-targetId", args: args{ @@ -105,19 +134,6 @@ func TestSession_Create(t *testing.T) { wantErr: true, wantIsErr: errors.InvalidParameter, }, - { - name: "empty-hostSetId", - args: args{ - composedOf: func() ComposedOf { - c := composedOf - c.HostSetId = "" - return c - }(), - addresses: defaultAddresses, - }, - wantErr: true, - wantIsErr: errors.InvalidParameter, - }, { name: "empty-authTokenId", args: args{ diff --git a/internal/session/testing.go b/internal/session/testing.go index 99cfc8125..8bb486176 100644 --- a/internal/session/testing.go +++ b/internal/session/testing.go @@ -67,6 +67,28 @@ func TestState(t testing.TB, conn *db.DB, sessionId string, state Status) *State return s } +// TestSessionHostSetHost creates a test session to host set host association for the sessionId in the repository. +func TestSessionHostSetHost(t testing.TB, conn *db.DB, sessionId, hostSetId, hostId string) { + t.Helper() + require := require.New(t) + rw := db.New(conn) + hs, err := NewSessionHostSetHost(sessionId, hostSetId, hostId) + require.NoError(err) + err = rw.Create(context.Background(), hs) + require.NoError(err) +} + +// TestSessionTargetAddress creates a test session to target address association for the sessionId in the repository. +func TestSessionTargetAddress(t testing.TB, conn *db.DB, sessionId, targetId string) { + t.Helper() + require := require.New(t) + rw := db.New(conn) + ta, err := NewSessionTargetAddress(sessionId, targetId) + require.NoError(err) + err = rw.Create(context.Background(), ta) + require.NoError(err) +} + // TestSession creates a test session composed of c in the repository. Options // are passed into New, and withServerId is handled locally. func TestSession(t testing.TB, conn *db.DB, rootWrapper wrapping.Wrapper, c ComposedOf, opt ...Option) *Session { @@ -113,6 +135,12 @@ func TestSession(t testing.TB, conn *db.DB, rootWrapper wrapping.Wrapper, c Comp require.NoError(err) } + if s.HostId != "" && s.HostSetId != "" { + TestSessionHostSetHost(t, conn, s.PublicId, s.HostSetId, s.HostId) + } else if s.Endpoint != "" { + TestSessionTargetAddress(t, conn, s.PublicId, s.TargetId) + } + ss, err := fetchStates(ctx, rw, s.PublicId, append(opts.withDbOpts, db.WithOrder("start_time desc"))...) require.NoError(err) s.States = ss @@ -120,6 +148,48 @@ func TestSession(t testing.TB, conn *db.DB, rootWrapper wrapping.Wrapper, c Comp return s } +func TestSessionWithTargetAddress(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository, opt ...Option) *Session { + t.Helper() + composedOf := TestSessionTargetAddressParams(t, conn, wrapper, iamRepo) + future := timestamppb.New(time.Now().Add(time.Hour)) + exp := ×tamp.Timestamp{Timestamp: future} + return TestSession(t, conn, wrapper, composedOf, append(opt, WithExpirationTime(exp))...) +} + +func TestSessionTargetAddressParams(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository) ComposedOf { + t.Helper() + ctx := context.Background() + + require := require.New(t) + rw := db.New(conn) + org, proj := iam.TestScopes(t, iamRepo) + + tcpTarget := tcp.TestTarget(ctx, t, conn, proj.PublicId, "test target") + target.TestTargetAddress(t, conn, tcpTarget.GetPublicId(), "tcp://127.0.0.1:22") + + kms := kms.TestKms(t, conn, wrapper) + authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0] + acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "name1") + user := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(acct.PublicId)) + + authTokenRepo, err := authtoken.NewRepository(rw, rw, kms) + require.NoError(err) + at, err := authTokenRepo.CreateAuthToken(ctx, user, acct.GetPublicId()) + require.NoError(err) + + expTime := timestamppb.Now() + expTime.Seconds += int64(tcpTarget.GetSessionMaxSeconds()) + return ComposedOf{ + UserId: user.PublicId, + TargetId: tcpTarget.GetPublicId(), + AuthTokenId: at.PublicId, + ProjectId: tcpTarget.GetProjectId(), + Endpoint: "tcp://127.0.0.1:22", + ExpirationTime: ×tamp.Timestamp{Timestamp: expTime}, + ConnectionLimit: tcpTarget.GetSessionConnectionLimit(), + } +} + // TestDefaultSession creates a test session in the repository using defaults. func TestDefaultSession(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository, opt ...Option) *Session { t.Helper() diff --git a/internal/target/address.go b/internal/target/address.go new file mode 100644 index 000000000..72d05584a --- /dev/null +++ b/internal/target/address.go @@ -0,0 +1,113 @@ +package target + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultTargetAddressTableName = "target_address" +) + +// An Address represents the relationship between a target and a +// network address. +type Address struct { + *store.TargetAddress + tableName string `gorm:"-"` +} + +// Ensure Address implements interfaces +var ( + _ db.VetForWriter = (*Address)(nil) + _ oplog.ReplayableMessage = (*Address)(nil) +) + +// NewAddress creates a new in memory address. No options are +// currently supported. +func NewAddress(targetId, address string, _ ...Option) (*Address, error) { + const op = "target.NewAddress" + if targetId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing target id") + } + if address == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing address") + } + t := &Address{ + TargetAddress: &store.TargetAddress{ + TargetId: targetId, + Address: address, + }, + } + return t, nil +} + +// Clone creates a clone of the target address +func (t *Address) Clone() any { + cp := proto.Clone(t.TargetAddress) + return &Address{ + TargetAddress: cp.(*store.TargetAddress), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the target +// address before it's written. +func (t *Address) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error { + const op = "target.(Address).VetForWrite" + if opType == db.CreateOp { + if t.GetTargetId() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing target id") + } + if t.GetAddress() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing address") + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (t *Address) TableName() string { + if t.tableName != "" { + return t.tableName + } + return DefaultTargetAddressTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (t *Address) SetTableName(n string) { + t.tableName = n +} + +func (t *Address) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{t.GetTargetId()}, + "resource-type": []string{"target address"}, + "op-type": []string{op.String()}, + } + return metadata +} + +func (t *Address) TargetId() string { + return t.GetTargetId() +} + +func (t *Address) Address() string { + return t.GetAddress() +} + +func (t *Address) GetPublicId() string { + return t.GetTargetId() +} + +// allocTargetAddress will allocate a address +func allocTargetAddress() *Address { + return &Address{ + TargetAddress: &store.TargetAddress{}, + } +} diff --git a/internal/target/address_test.go b/internal/target/address_test.go new file mode 100644 index 000000000..25bfbeb89 --- /dev/null +++ b/internal/target/address_test.go @@ -0,0 +1,67 @@ +package target_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddress_New(t *testing.T) { + type args struct { + targetId string + address string + } + tests := []struct { + name string + args args + want *target.Address + wantErr errors.Code + }{ + { + name: "no-target_id", + args: args{ + address: "0.0.0.0", + }, + wantErr: errors.InvalidParameter, + }, + { + name: "no-address", + args: args{ + targetId: "targ_0000000", + }, + wantErr: errors.InvalidParameter, + }, + { + name: "valid", + args: args{ + targetId: "targ_0000000", + address: "0.0.0.0", + }, + want: &target.Address{ + TargetAddress: &store.TargetAddress{ + TargetId: "targ_0000000", + Address: "0.0.0.0", + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := target.NewAddress(tt.args.targetId, tt.args.address) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + require.NotNil(got) + assert.EqualValues(tt.want, got) + }) + } +} diff --git a/internal/target/options.go b/internal/target/options.go index 2f3b44786..5eabe1a4f 100644 --- a/internal/target/options.go +++ b/internal/target/options.go @@ -41,6 +41,7 @@ type options struct { WithEgressWorkerFilter string WithIngressWorkerFilter string WithTargetIds []string + WithAddress string } func getDefaultOptions() options { @@ -64,6 +65,7 @@ func getDefaultOptions() options { WithWorkerFilter: "", WithEgressWorkerFilter: "", WithIngressWorkerFilter: "", + WithAddress: "", } } @@ -207,3 +209,10 @@ func WithPermissions(perms []perms.Permission) Option { o.WithPermissions = perms } } + +// WithAddress provides an optional network address +func WithAddress(address string) Option { + return func(o *options) { + o.WithAddress = address + } +} diff --git a/internal/target/repository.go b/internal/target/repository.go index cd7c50c54..d929b4a42 100644 --- a/internal/target/repository.go +++ b/internal/target/repository.go @@ -125,6 +125,7 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op target := allocTargetView() target.PublicId = publicIdOrName + var address string var hostSources []HostSource var credSources []CredentialSource _, err := r.writer.DoTx( @@ -150,6 +151,13 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op if credSources, err = fetchCredentialSources(ctx, read, target.PublicId); err != nil { return errors.Wrap(ctx, err, op) } + targetAddress, err := fetchAddress(ctx, read, target.PublicId) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op) + } + if targetAddress != nil { + address = targetAddress.GetAddress() + } return nil }, ) @@ -159,7 +167,7 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op } return nil, nil, nil, errors.Wrap(ctx, err, op) } - subtype, err := target.targetSubtype(ctx) + subtype, err := target.targetSubtype(ctx, address) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op) } @@ -242,9 +250,28 @@ func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, return nil, errors.Wrap(ctx, err, op) } + var targetIds []string + for _, t := range foundTargets { + targetIds = append(targetIds, t.GetPublicId()) + } + + addresses := map[string]string{} + var foundAddresses []*Address + err = r.reader.SearchWhere(ctx, &foundAddresses, "target_id in (?)", []any{targetIds}) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, addr := range foundAddresses { + addresses[addr.TargetId()] = addr.Address() + } + targets := make([]Target, 0, len(foundTargets)) for _, t := range foundTargets { - subtype, err := t.targetSubtype(ctx) + var address string + if v, ok := addresses[t.GetPublicId()]; ok { + address = v + } + subtype, err := t.targetSubtype(ctx, address) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -336,80 +363,6 @@ func (r *Repository) DeleteTarget(ctx context.Context, publicId string, _ ...Opt return rowsDeleted, nil } -// update a target in the db repository with an oplog entry. -// It currently supports no options. -func (r *Repository) update(ctx context.Context, target Target, version uint32, fieldMaskPaths []string, setToNullPaths []string, _ ...Option) (Target, []HostSource, []CredentialSource, int, error) { - const op = "target.(Repository).update" - if version == 0 { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") - } - if target == nil { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "nil target") - } - cloner, ok := target.(Cloneable) - if !ok { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "target is not cloneable") - } - dbOpts := []db.Option{ - db.WithVersion(&version), - } - projectId := target.GetProjectId() - if projectId == "" { - t := allocTargetView() - t.PublicId = target.GetPublicId() - if err := r.reader.LookupByPublicId(ctx, &t); err != nil { - return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("lookup failed for %s", t.PublicId))) - } - projectId = t.ProjectId - } - oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) - if err != nil { - return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) - } - metadata := target.Oplog(oplog.OpType_OP_TYPE_UPDATE) - dbOpts = append(dbOpts, db.WithOplog(oplogWrapper, metadata)) - - var rowsUpdated int - var returnedTarget any - var hostSources []HostSource - var credSources []CredentialSource - _, err = r.writer.DoTx( - ctx, - db.StdRetryCnt, - db.ExpBackoff{}, - func(reader db.Reader, w db.Writer) error { - returnedTarget = cloner.Clone() - rowsUpdated, err = w.Update( - ctx, - returnedTarget, - fieldMaskPaths, - setToNullPaths, - dbOpts..., - ) - if err != nil { - return errors.Wrap(ctx, err, op) - } - if rowsUpdated > 1 { - // return err, which will result in a rollback of the update - return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") - } - - if hostSources, err = fetchHostSources(ctx, reader, target.GetPublicId()); err != nil { - return errors.Wrap(ctx, err, op) - } - - if credSources, err = fetchCredentialSources(ctx, reader, target.GetPublicId()); err != nil { - return errors.Wrap(ctx, err, op) - } - return nil - }, - ) - if err != nil { - return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) - } - return returnedTarget.(Target), hostSources, credSources, rowsUpdated, nil -} - // CreateTarget inserts into the repository and returns the new Target with // its list of host sets and credential libraries. // WithPublicId is the only supported option. @@ -455,6 +408,15 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt t.SetPublicId(ctx, id) } + var address *Address + var err error + if t.GetAddress() != "" { + address, err = NewAddress(t.GetPublicId(), t.GetAddress()) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op) + } + } + oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) @@ -480,6 +442,15 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target")) } msgs = append(msgs, &targetOplogMsg) + + if address != nil { + var targetAddressOplogMsg oplog.Message + if err := w.Create(ctx, address, db.NewOplogMsg(&targetAddressOplogMsg)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target address")) + } + msgs = append(msgs, &targetAddressOplogMsg) + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) } @@ -504,6 +475,15 @@ func (r *Repository) UpdateTarget(ctx context.Context, target Target, version ui if target == nil { return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target") } + if target.GetPublicId() == "" { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target public id") + } + if target.GetProjectId() == "" { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if version == 0 { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } vet, ok := subtypeRegistry.vetForUpdateFunc(target.GetType()) if !ok { return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType())) @@ -512,10 +492,7 @@ func (r *Repository) UpdateTarget(ctx context.Context, target Target, version ui return nil, nil, nil, db.NoRowsAffected, err } - if target.GetPublicId() == "" { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target public id") - } - + var addressEndpoint string for _, f := range fieldMaskPaths { switch { case strings.EqualFold("name", f): @@ -526,10 +503,13 @@ func (r *Repository) UpdateTarget(ctx context.Context, target Target, version ui case strings.EqualFold("workerfilter", f): case strings.EqualFold("egressworkerfilter", f): case strings.EqualFold("ingressworkerfilter", f): + case strings.EqualFold("address", f): + addressEndpoint = target.GetAddress() default: return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, fmt.Sprintf("invalid field mask: %s", f)) } } + var dbMask, nullFields []string dbMask, nullFields = dbw.BuildUpdatePaths( map[string]any{ @@ -541,6 +521,7 @@ func (r *Repository) UpdateTarget(ctx context.Context, target Target, version ui "WorkerFilter": target.GetWorkerFilter(), "EgressWorkerFilter": target.GetEgressWorkerFilter(), "IngressWorkerFilter": target.GetIngressWorkerFilter(), + "Address": target.GetAddress(), }, fieldMaskPaths, []string{"SessionMaxSeconds", "SessionConnectionLimit"}, @@ -548,21 +529,110 @@ func (r *Repository) UpdateTarget(ctx context.Context, target Target, version ui if len(dbMask) == 0 && len(nullFields) == 0 { return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask") } - var returnedTarget Target + + // The Address field is not apart of the target schema in the database. + // It is apart of a different table called target_address, which is why + // the Address field must be filtered out of the dbMask & nullFields slices. + var updateAddress, deleteAddress bool + var filteredDbMask, filteredNullFields []string + for _, f := range dbMask { + switch { + case strings.EqualFold("Address", f): + updateAddress = true + default: + filteredDbMask = append(filteredDbMask, f) + } + } + for _, f := range nullFields { + switch { + case strings.EqualFold("Address", f): + deleteAddress = true + default: + filteredNullFields = append(filteredNullFields, f) + } + } + + // If the Address field is the only present change, then we must still + // update the target's version because a target address is a child object + // of the target. + if (len(filteredDbMask) == 0 && len(filteredNullFields) == 0) && (updateAddress || deleteAddress) { + target.SetVersion(version + 1) + filteredDbMask = append(filteredDbMask, "Version") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeOplog) + if err != nil { + return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + var rowsUpdated int + var returnedTarget Target var hostSources []HostSource var credSources []CredentialSource - _, err := r.writer.DoTx( + _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(read db.Reader, w db.Writer) error { - var err error t := target.Clone() - returnedTarget, hostSources, credSources, rowsUpdated, err = r.update(ctx, t, version, dbMask, nullFields) + rowsUpdated, err = w.Update(ctx, t, filteredDbMask, filteredNullFields, + db.WithOplog(oplogWrapper, t.Oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version), + ) if err != nil { return errors.Wrap(ctx, err, op) } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 target resource would have been updated") + } + + if hostSources, err = fetchHostSources(ctx, read, t.GetPublicId()); err != nil { + return errors.Wrap(ctx, err, op) + } + if credSources, err = fetchCredentialSources(ctx, read, t.GetPublicId()); err != nil { + return errors.Wrap(ctx, err, op) + } + + var address *Address + switch { + case updateAddress: + if len(hostSources) > 0 { + return errors.New(ctx, errors.Conflict, op, "unable to set address because one or more host sources is assigned to the given target") + } + address, err = NewAddress(t.GetPublicId(), addressEndpoint) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err := w.Create(ctx, address, + db.WithOplog(oplogWrapper, address.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithOnConflict(&db.OnConflict{ + Target: db.Columns{"target_id"}, + Action: db.UpdateAll(true), + })); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target address")) + } + case deleteAddress: + var err error + address = allocTargetAddress() + address.TargetAddress.TargetId = t.GetPublicId() + rowsUpdated, err = w.Delete(ctx, address, db.WithOplog(oplogWrapper, address.oplog(oplog.OpType_OP_TYPE_DELETE))) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete target address")) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 target resource would have been deleted") + } + default: + address, err = fetchAddress(ctx, read, t.GetPublicId()) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to fetch target address")) + } + } + if address != nil { + t.SetAddress(address.GetAddress()) + } + returnedTarget = t.Clone() + return nil }, ) diff --git a/internal/target/repository_address.go b/internal/target/repository_address.go new file mode 100644 index 000000000..85f2cadbf --- /dev/null +++ b/internal/target/repository_address.go @@ -0,0 +1,21 @@ +package target + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +func fetchAddress(ctx context.Context, r db.Reader, targetId string) (*Address, error) { + const op = "target.fetchAddress" + var address *Address + if err := r.SearchWhere(ctx, &address, "target_id = ?", []any{targetId}); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if address.TargetAddress == nil { + return nil, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("%s target address not found", targetId), errors.WithoutEvent()) + } + return address, nil +} diff --git a/internal/target/repository_credential_source.go b/internal/target/repository_credential_source.go index fac23638c..8331b7738 100644 --- a/internal/target/repository_credential_source.go +++ b/internal/target/repository_credential_source.go @@ -60,7 +60,7 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st var hostSources []HostSource var credSources []CredentialSource - var updatedTarget any + var updatedTarget Target _, err = r.writer.DoTx( ctx, db.StdRetryCnt, @@ -72,7 +72,7 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedTarget = target.(Cloneable).Clone() + updatedTarget = target.Clone() var targetOplogMsg oplog.Message rowsUpdated, err := w.Update(ctx, updatedTarget, []string{"Version"}, nil, db.NewOplogMsg(&targetOplogMsg), db.WithVersion(&targetVersion)) if err != nil { @@ -121,6 +121,13 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve credential sources after adding")) } + address, err := fetchAddress(ctx, reader, targetId) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve target address after adding")) + } + if address != nil { + updatedTarget.SetAddress(address.GetAddress()) + } return nil }, ) diff --git a/internal/target/repository_host_source.go b/internal/target/repository_host_source.go index 338431ed2..56d600cd1 100644 --- a/internal/target/repository_host_source.go +++ b/internal/target/repository_host_source.go @@ -58,18 +58,27 @@ func (r *Repository) AddTargetHostSources(ctx context.Context, targetId string, } var currentHostSources []HostSource var currentCredSources []CredentialSource - var updatedTarget any + var updatedTarget Target _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { + address, err := fetchAddress(ctx, reader, targetId) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current target address")) + } + if address != nil && address.GetAddress() != "" { + return errors.New(ctx, errors.Conflict, op, "unable to add host sources because a network address is directly assigned to the given target") + } + msgs := make([]*oplog.Message, 0, 2) targetTicket, err := w.GetTicket(ctx, target) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedTarget = target.(Cloneable).Clone() + + updatedTarget = target.Clone() var targetOplogMsg oplog.Message rowsUpdated, err := w.Update(ctx, updatedTarget, []string{"Version"}, nil, db.NewOplogMsg(&targetOplogMsg), db.WithVersion(&targetVersion)) if err != nil { @@ -103,7 +112,8 @@ func (r *Repository) AddTargetHostSources(ctx context.Context, targetId string, if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating sets")) } - return updatedTarget.(Target), currentHostSources, currentCredSources, nil + + return updatedTarget, currentHostSources, currentCredSources, nil } // DeleteTargeHostSources deletes host sources from a target (targetId). The @@ -284,6 +294,14 @@ func (r *Repository) SetTargetHostSources(ctx context.Context, targetId string, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { + address, err := fetchAddress(ctx, reader, targetId) + if err != nil && !errors.IsNotFoundError(err) { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current target address")) + } + if address != nil && address.GetAddress() != "" { + return errors.New(ctx, errors.Conflict, op, "unable to set host sources because a network address is directly assigned to the given target") + } + msgs := make([]*oplog.Message, 0, 2) targetTicket, err := w.GetTicket(ctx, target) if err != nil { diff --git a/internal/target/store/target.pb.go b/internal/target/store/target.pb.go index 8473e3f74..56fda3e02 100644 --- a/internal/target/store/target.pb.go +++ b/internal/target/store/target.pb.go @@ -8,6 +8,7 @@ package store import ( timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -270,6 +271,65 @@ func (x *TargetHostSet) GetCreateTime() *timestamp.Timestamp { return nil } +type TargetAddress struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // target_id of the Target + // @inject_tag: gorm:"primary_key" + TargetId string `protobuf:"bytes,10,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty" gorm:"primary_key"` + // address associated to the Target + // @inject_tag: `gorm:"not_null"` + Address string `protobuf:"bytes,20,opt,name=address,proto3" json:"address,omitempty" gorm:"not_null"` +} + +func (x *TargetAddress) Reset() { + *x = TargetAddress{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TargetAddress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TargetAddress) ProtoMessage() {} + +func (x *TargetAddress) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TargetAddress.ProtoReflect.Descriptor instead. +func (*TargetAddress) Descriptor() ([]byte, []int) { + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{2} +} + +func (x *TargetAddress) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *TargetAddress) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + type CredentialLibrary struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -292,7 +352,7 @@ type CredentialLibrary struct { func (x *CredentialLibrary) Reset() { *x = CredentialLibrary{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -305,7 +365,7 @@ func (x *CredentialLibrary) String() string { func (*CredentialLibrary) ProtoMessage() {} func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -318,7 +378,7 @@ func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialLibrary.ProtoReflect.Descriptor instead. func (*CredentialLibrary) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{2} + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{3} } func (x *CredentialLibrary) GetTargetId() string { @@ -371,7 +431,7 @@ type StaticCredential struct { func (x *StaticCredential) Reset() { *x = StaticCredential{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -384,7 +444,7 @@ func (x *StaticCredential) String() string { func (*StaticCredential) ProtoMessage() {} func (x *StaticCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -397,7 +457,7 @@ func (x *StaticCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredential.ProtoReflect.Descriptor instead. func (*StaticCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{3} + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{4} } func (x *StaticCredential) GetTargetId() string { @@ -453,7 +513,7 @@ type CredentialSource struct { func (x *CredentialSource) Reset() { *x = CredentialSource{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[4] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -466,7 +526,7 @@ func (x *CredentialSource) String() string { func (*CredentialSource) ProtoMessage() {} func (x *CredentialSource) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[4] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -479,7 +539,7 @@ func (x *CredentialSource) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialSource.ProtoReflect.Descriptor instead. func (*CredentialSource) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{4} + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{5} } func (x *CredentialSource) GetTargetId() string { @@ -533,7 +593,7 @@ type CredentialSourceView struct { func (x *CredentialSourceView) Reset() { *x = CredentialSourceView{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[5] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -546,7 +606,7 @@ func (x *CredentialSourceView) String() string { func (*CredentialSourceView) ProtoMessage() {} func (x *CredentialSourceView) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[5] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -559,7 +619,7 @@ func (x *CredentialSourceView) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialSourceView.ProtoReflect.Descriptor instead. func (*CredentialSourceView) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{5} + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{6} } func (x *CredentialSourceView) GetPublicId() string { @@ -584,109 +644,118 @@ var file_controller_storage_target_store_v1_target_proto_rawDesc = []byte{ 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x22, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe0, 0x04, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x56, 0x69, 0x65, 0x77, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, + 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, + 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xe0, 0x04, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x69, 0x65, + 0x77, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, + 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x38, + 0x0a, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x16, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x31, 0x0a, + 0x14, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x33, 0x0a, 0x15, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x8c, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x13, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x99, 0x01, 0x0a, 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x53, + 0x65, 0x74, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x22, 0x5e, 0x0a, 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, + 0x30, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x16, 0xc2, 0xdd, 0x29, 0x12, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x22, 0xe0, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, + 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, - 0x5a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, - 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, 0x6f, 0x6e, - 0x64, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x6e, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x23, 0x0a, 0x0d, - 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x78, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x12, 0x31, 0x0a, 0x14, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x6f, 0x72, 0x6b, - 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x12, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x15, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, - 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x8c, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x57, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x99, 0x01, 0x0a, 0x0d, 0x54, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, - 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x68, - 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, - 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x10, 0x53, 0x74, 0x61, - 0x74, 0x69, 0x63, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, - 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, - 0x2d, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, - 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, - 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x10, - 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, - 0x14, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, - 0x2d, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, - 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, - 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x47, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x56, 0x69, 0x65, 0x77, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x69, 0x6d, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x10, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, + 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, + 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x47, 0x0a, 0x14, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x69, 0x65, 0x77, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -701,23 +770,24 @@ func file_controller_storage_target_store_v1_target_proto_rawDescGZIP() []byte { return file_controller_storage_target_store_v1_target_proto_rawDescData } -var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_controller_storage_target_store_v1_target_proto_goTypes = []interface{}{ (*TargetView)(nil), // 0: controller.storage.target.store.v1.TargetView (*TargetHostSet)(nil), // 1: controller.storage.target.store.v1.TargetHostSet - (*CredentialLibrary)(nil), // 2: controller.storage.target.store.v1.CredentialLibrary - (*StaticCredential)(nil), // 3: controller.storage.target.store.v1.StaticCredential - (*CredentialSource)(nil), // 4: controller.storage.target.store.v1.CredentialSource - (*CredentialSourceView)(nil), // 5: controller.storage.target.store.v1.CredentialSourceView - (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp + (*TargetAddress)(nil), // 2: controller.storage.target.store.v1.TargetAddress + (*CredentialLibrary)(nil), // 3: controller.storage.target.store.v1.CredentialLibrary + (*StaticCredential)(nil), // 4: controller.storage.target.store.v1.StaticCredential + (*CredentialSource)(nil), // 5: controller.storage.target.store.v1.CredentialSource + (*CredentialSourceView)(nil), // 6: controller.storage.target.store.v1.CredentialSourceView + (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_target_store_v1_target_proto_depIdxs = []int32{ - 6, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 3: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 4: controller.storage.target.store.v1.StaticCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 5: controller.storage.target.store.v1.CredentialSource.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 3: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 4: controller.storage.target.store.v1.StaticCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 5: controller.storage.target.store.v1.CredentialSource.create_time:type_name -> controller.storage.timestamp.v1.Timestamp 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name @@ -756,7 +826,7 @@ func file_controller_storage_target_store_v1_target_proto_init() { } } file_controller_storage_target_store_v1_target_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CredentialLibrary); i { + switch v := v.(*TargetAddress); i { case 0: return &v.state case 1: @@ -768,7 +838,7 @@ func file_controller_storage_target_store_v1_target_proto_init() { } } file_controller_storage_target_store_v1_target_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StaticCredential); i { + switch v := v.(*CredentialLibrary); i { case 0: return &v.state case 1: @@ -780,7 +850,7 @@ func file_controller_storage_target_store_v1_target_proto_init() { } } file_controller_storage_target_store_v1_target_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CredentialSource); i { + switch v := v.(*StaticCredential); i { case 0: return &v.state case 1: @@ -792,6 +862,18 @@ func file_controller_storage_target_store_v1_target_proto_init() { } } file_controller_storage_target_store_v1_target_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CredentialSource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_target_store_v1_target_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CredentialSourceView); i { case 0: return &v.state @@ -810,7 +892,7 @@ func file_controller_storage_target_store_v1_target_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_target_store_v1_target_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/target/target.go b/internal/target/target.go index 21ad9f136..50ac8550e 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -28,6 +28,7 @@ type Target interface { GetWorkerFilter() string GetEgressWorkerFilter() string GetIngressWorkerFilter() string + GetAddress() string Clone() Target SetPublicId(context.Context, string) error SetProjectId(string) @@ -42,6 +43,7 @@ type Target interface { SetWorkerFilter(string) SetEgressWorkerFilter(string) SetIngressWorkerFilter(string) + SetAddress(string) Oplog(op oplog.OpType) oplog.Metadata } @@ -55,6 +57,8 @@ var _ boundary.AuthzProtectedEntity = (*targetView)(nil) // underlying type. type targetView struct { *store.TargetView + // Network address assigned to the Target. + Address string `json:"address,omitempty" gorm:"-"` tableName string `gorm:"-"` } @@ -105,7 +109,7 @@ func (t *targetView) Subtype() subtypes.Subtype { } // targetSubtype converts the target view to the concrete subtype -func (t *targetView) targetSubtype(ctx context.Context) (Target, error) { +func (t *targetView) targetSubtype(ctx context.Context, address string) (Target, error) { const op = "target.targetView.targetSubtype" alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) @@ -129,5 +133,6 @@ func (t *targetView) targetSubtype(ctx context.Context) (Target, error) { tt.SetWorkerFilter(t.WorkerFilter) tt.SetEgressWorkerFilter(t.EgressWorkerFilter) tt.SetIngressWorkerFilter(t.IngressWorkerFilter) + tt.SetAddress(address) return tt, nil } diff --git a/internal/target/targettest/target.go b/internal/target/targettest/target.go index 108c5e8cd..160f4a268 100644 --- a/internal/target/targettest/target.go +++ b/internal/target/targettest/target.go @@ -27,6 +27,7 @@ const ( // Target is a target.Target used for tests. type Target struct { *store.Target + Address string `gorm:"-"` tableName string `gorm:"-"` } @@ -125,10 +126,15 @@ func (t *Target) GetIngressWorkerFilter() string { return t.IngressWorkerFilter } +func (t *Target) GetAddress() string { + return t.Address +} + func (t *Target) Clone() target.Target { cp := proto.Clone(t.Target) return &Target{ - Target: cp.(*store.Target), + Address: t.Address, + Target: cp.(*store.Target), } } @@ -185,6 +191,10 @@ func (t *Target) SetIngressWorkerFilter(filter string) { t.IngressWorkerFilter = filter } +func (t *Target) SetAddress(a string) { + t.Address = a +} + func (t *Target) Oplog(op oplog.OpType) oplog.Metadata { return oplog.Metadata{ "resource-public-id": []string{t.PublicId}, diff --git a/internal/target/tcp/exports_test.go b/internal/target/tcp/exports_test.go index 5c405983c..08b8ac5aa 100644 --- a/internal/target/tcp/exports_test.go +++ b/internal/target/tcp/exports_test.go @@ -1,6 +1,9 @@ package tcp -import "github.com/hashicorp/boundary/internal/target" +import ( + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/store" +) // Expose functions and variables for tests. var ( @@ -17,3 +20,12 @@ func NewTestTarget(projectId string, opt ...target.Option) target.Target { t.SetProjectId(projectId) return t } + +// NewTestAddress is a test helper that bypasses the targetId & address checks +// performed by NewAddress, allowing tests to create a target Address with +// nil fields for more robust testing. +func NewTestAddress() *target.Address { + return &target.Address{ + TargetAddress: &store.TargetAddress{}, + } +} diff --git a/internal/target/tcp/repository_host_source_test.go b/internal/target/tcp/repository_host_source_test.go index 09c60c936..d85fecfd6 100644 --- a/internal/target/tcp/repository_host_source_test.go +++ b/internal/target/tcp/repository_host_source_test.go @@ -50,13 +50,15 @@ func TestRepository_AddTargetHostSets(t *testing.T) { type args struct { targetVersion uint32 wantTargetIds bool + wantAddress bool opt []target.Option } tests := []struct { - name string - args args - wantErr bool - wantErrIs error + name string + args args + wantErr bool + wantErrMsg string + wantErrIs error }{ { name: "valid", @@ -66,6 +68,16 @@ func TestRepository_AddTargetHostSets(t *testing.T) { }, wantErr: false, }, + { + name: "address-mutually-exclusive-relationship", + args: args{ + targetVersion: 1, + wantAddress: true, + wantTargetIds: true, + }, + wantErr: true, + wantErrMsg: "unable to add host sources because a network address is directly assigned to the given target", + }, { name: "bad-version", args: args{ @@ -99,11 +111,20 @@ func TestRepository_AddTargetHostSets(t *testing.T) { ctx := context.Background() projTarget := tcp.TestTarget(ctx, t, conn, staticProj.PublicId, "static-proj") + var address *target.Address + if tt.args.wantAddress { + address = target.TestTargetAddress(t, conn, projTarget.GetPublicId(), "8.8.8.8") + } + var hostSourceIds []string origTarget, origHostSet, _, err := repo.LookupTarget(ctx, projTarget.GetPublicId()) require.NoError(err) require.Equal(0, len(origHostSet)) + if address != nil { + require.Equal(address.GetAddress(), origTarget.GetAddress()) + } + if tt.args.wantTargetIds { hostSourceIds = createHostSetsFn([]string{staticProj.PublicId}) } @@ -114,6 +135,9 @@ func TestRepository_AddTargetHostSets(t *testing.T) { if tt.wantErrIs != nil { assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error %s", err.Error()) } + if tt.wantErrMsg != "" { + assert.Contains(err.Error(), tt.wantErrMsg) + } // test to see of the target version update oplog was not created err = db.TestVerifyOplog(t, rw, projTarget.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.Error(err) @@ -448,6 +472,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { setup func(target.Target) []target.HostSource args args wantAffectedRows int + wantErrMsg string wantErr bool }{ { @@ -461,6 +486,17 @@ func TestRepository_SetTargetHostSets(t *testing.T) { wantErr: false, wantAffectedRows: 10, }, + { + name: "address-mutually-exclusive-relationship", + args: args{ + target: tcp.TestTarget(ctx, t, conn, proj.PublicId, "invalid-host-source", target.WithAddress("8.8.8.8")), + targetVersion: 2, // yep, since setupFn will increment it to 2 + hostSourceIds: []string{testHostSetIds[0], testHostSetIds[1]}, + addToOrigHostSources: true, + }, + wantErr: true, + wantErrMsg: "unable to set host sources because a network address is directly assigned to the given target", + }, { name: "no-change", setup: setupFn, diff --git a/internal/target/tcp/repository_tcp_target_test.go b/internal/target/tcp/repository_tcp_target_test.go index 6dd757e3f..919ae895f 100644 --- a/internal/target/tcp/repository_tcp_target_test.go +++ b/internal/target/tcp/repository_tcp_target_test.go @@ -64,6 +64,21 @@ func TestRepository_CreateTarget(t *testing.T) { }, wantErr: false, }, + { + name: "with-address", + args: args{ + target: func() target.Target { + target, err := target.New(ctx, tcp.Subtype, proj.PublicId, + target.WithName("with-address"), + target.WithDescription("with-address"), + target.WithDefaultPort(80), + target.WithAddress("8.8.8.8")) + require.NoError(t, err) + return target + }(), + }, + wantErr: false, + }, { name: "nil-target", args: args{ @@ -187,6 +202,7 @@ func TestRepository_CreateTarget(t *testing.T) { foundTarget, foundHostSources, foundCredLibs, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) assert.NoError(err) + assert.Equal(tt.args.target.GetAddress(), tar.GetAddress()) assert.True(proto.Equal(tar.(*tcp.Target), foundTarget.(*tcp.Target))) assert.Equal(hostSources, foundHostSources) assert.Equal(credSources, foundCredLibs) @@ -224,6 +240,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { type args struct { name string description string + address string port uint32 fieldMaskPaths []string opt []target.Option @@ -231,16 +248,17 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { PublicId *string } tests := []struct { - name string - newProjectId string - newName string - newTargetOpts []target.Option - args args - wantRowsUpdate int - wantErr bool - wantErrMsg string - wantIsError errors.Code - wantDup bool + name string + newProjectId string + newName string + newTargetOpts []target.Option + args args + wantRowsUpdate int + wantErr bool + wantErrMsg string + wantIsError errors.Code + wantDup bool + wantHostSources bool }{ { name: "valid", @@ -249,9 +267,63 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Name"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: false, - wantRowsUpdate: 1, + newProjectId: proj.PublicId, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: true, + }, + + { + name: "valid-address", + args: args{ + name: "valid-address" + id, + fieldMaskPaths: []string{"Name", "Address"}, + ProjectId: proj.PublicId, + address: "8.8.8.8", + }, + newProjectId: proj.PublicId, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: false, + }, + { + name: "null-address", + args: args{ + fieldMaskPaths: []string{"Address"}, + ProjectId: proj.PublicId, + address: "null", + }, + newProjectId: proj.PublicId, + newTargetOpts: []target.Option{target.WithAddress("8.8.8.8")}, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: false, + }, + { + name: "delete-address", + args: args{ + fieldMaskPaths: []string{"Address"}, + ProjectId: proj.PublicId, + }, + newProjectId: proj.PublicId, + newTargetOpts: []target.Option{target.WithAddress("8.8.8.8")}, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: false, + }, + { + name: "host-source-mutually-exclusive-relationship", + args: args{ + name: "invalid-address" + id, + address: "8.8.8.8", + fieldMaskPaths: []string{"Name", "Address"}, + ProjectId: proj.PublicId, + }, + newProjectId: proj.PublicId, + wantErr: true, + wantErrMsg: "unable to set address because one or more host sources is assigned to the given target", + wantRowsUpdate: 0, + wantHostSources: true, }, { name: "valid-no-op", @@ -260,10 +332,11 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Name"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - newName: "valid-no-op" + id, - wantErr: false, - wantRowsUpdate: 1, + newProjectId: proj.PublicId, + newName: "valid-no-op" + id, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: true, }, { name: "not-found", @@ -273,11 +346,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { ProjectId: proj.PublicId, PublicId: func() *string { s := "1"; return &s }(), }, - newProjectId: proj.PublicId, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "record not found, search issue: error #1100", - wantIsError: errors.RecordNotFound, + newProjectId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "record not found, search issue: error #1100", + wantIsError: errors.RecordNotFound, + wantHostSources: true, }, { name: "null-name", @@ -286,11 +360,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Name"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - newName: "null-name" + id, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "db.DoTx: target.(Repository).UpdateTarget: target.(Repository).update: db.DoTx: target.(Repository).update: db.Update: name must not be empty: not null constraint violated: integrity violation: error #1001", + newProjectId: proj.PublicId, + newName: "null-name" + id, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "db.DoTx: target.(Repository).UpdateTarget: db.Update: name must not be empty: not null constraint violated: integrity violation: error #1001", + wantHostSources: true, }, { name: "null-description", @@ -299,10 +374,11 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Description"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - newTargetOpts: []target.Option{target.WithDescription("null-description" + id)}, - wantErr: false, - wantRowsUpdate: 1, + newProjectId: proj.PublicId, + newTargetOpts: []target.Option{target.WithDescription("null-description" + id)}, + wantErr: false, + wantRowsUpdate: 1, + wantHostSources: true, }, { name: "empty-field-mask", @@ -311,11 +387,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", - wantIsError: errors.EmptyFieldMask, + newProjectId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", + wantIsError: errors.EmptyFieldMask, + wantHostSources: true, }, { name: "nil-fieldmask", @@ -324,11 +401,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: nil, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", - wantIsError: errors.EmptyFieldMask, + newProjectId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", + wantIsError: errors.EmptyFieldMask, + wantHostSources: true, }, { name: "read-only-fields", @@ -337,11 +415,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"CreateTime"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: CreateTime: parameter violation: error #103", - wantIsError: errors.InvalidFieldMask, + newProjectId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: CreateTime: parameter violation: error #103", + wantIsError: errors.InvalidFieldMask, + wantHostSources: true, }, { name: "unknown-fields", @@ -350,11 +429,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Alice"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: Alice: parameter violation: error #103", - wantIsError: errors.InvalidFieldMask, + newProjectId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: Alice: parameter violation: error #103", + wantIsError: errors.InvalidFieldMask, + wantHostSources: true, }, { name: "no-public-id", @@ -364,11 +444,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { ProjectId: proj.PublicId, PublicId: pubId(""), }, - newProjectId: proj.PublicId, - wantErr: true, - wantErrMsg: "target.(Repository).UpdateTarget: missing target public id: parameter violation: error #100", - wantIsError: errors.InvalidParameter, - wantRowsUpdate: 0, + newProjectId: proj.PublicId, + wantErr: true, + wantErrMsg: "target.(Repository).UpdateTarget: missing target public id: parameter violation: error #100", + wantIsError: errors.InvalidParameter, + wantRowsUpdate: 0, + wantHostSources: true, }, { name: "project-id-no-mask", @@ -376,21 +457,11 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { name: "project-id" + id, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", - wantIsError: errors.EmptyFieldMask, - }, - { - name: "empty-project-id-with-name-mask", - args: args{ - name: "empty-project-id" + id, - fieldMaskPaths: []string{"Name"}, - ProjectId: "", - }, - newProjectId: proj.PublicId, - wantErr: false, - wantRowsUpdate: 1, + newProjectId: proj.PublicId, + wantErr: true, + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", + wantIsError: errors.EmptyFieldMask, + wantHostSources: true, }, { name: "dup-name", @@ -399,11 +470,12 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { fieldMaskPaths: []string{"Name"}, ProjectId: proj.PublicId, }, - newProjectId: proj.PublicId, - wantErr: true, - wantDup: true, - wantErrMsg: " already exists in project " + proj.PublicId, - wantIsError: errors.NotUnique, + newProjectId: proj.PublicId, + wantErr: true, + wantDup: true, + wantErrMsg: " already exists in project " + proj.PublicId, + wantIsError: errors.NotUnique, + wantHostSources: true, }, } css := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), len(tests)) @@ -416,11 +488,18 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { _ = tcp.TestTarget(ctx, t, conn, proj.PublicId, tt.args.name) } - testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) - hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 5) - testHostSetIds := make([]string, 0, len(hsets)) - for _, hs := range hsets { - testHostSetIds = append(testHostSetIds, hs.PublicId) + testHostSetIds := []string{} + if tt.wantHostSources { + testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 5) + testHostSetIds = make([]string, 0, len(hsets)) + for _, hs := range hsets { + testHostSetIds = append(testHostSetIds, hs.PublicId) + } + tt.newTargetOpts = append( + tt.newTargetOpts, + target.WithHostSources(testHostSetIds), + ) } cls := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 5) @@ -438,7 +517,6 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { tt.newTargetOpts = append( tt.newTargetOpts, - target.WithHostSources(testHostSetIds), target.WithCredentialLibraries(testCredLibs), ) name := tt.newName @@ -451,6 +529,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { target.WithName(tt.args.name), target.WithDescription(tt.args.description), target.WithDefaultPort(tt.args.port), + target.WithAddress(tt.args.address), ) updateTarget.SetPublicId(ctx, tar.GetPublicId()) if tt.args.PublicId != nil { @@ -464,6 +543,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { assert.True(errors.Match(errors.T(tt.wantIsError), err)) assert.Nil(targetAfterUpdate) assert.Equal(0, updatedRows) + assert.NotEmpty(err.Error()) assert.Contains(err.Error(), tt.wantErrMsg) err = db.TestVerifyOplog(t, rw, tar.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.Error(err) @@ -478,6 +558,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { afterUpdateIds = append(afterUpdateIds, hs.Id()) } assert.Equal(testHostSetIds, afterUpdateIds) + assert.Equal(tt.args.address, targetAfterUpdate.GetAddress()) afterUpdateIds = make([]string, 0, len(credSources)) for _, cl := range credSources { @@ -487,13 +568,14 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { switch tt.name { case "valid-no-op": - assert.Equal(tar.GetUpdateTime(), targetAfterUpdate.GetUpdateTime()) + assert.Equal(tar.GetUpdateTime().String(), targetAfterUpdate.GetUpdateTime().String()) default: assert.NotEqual(tar.GetUpdateTime(), targetAfterUpdate.GetUpdateTime()) } foundTarget, _, _, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) assert.NoError(err) assert.True(proto.Equal(targetAfterUpdate.((*tcp.Target)), foundTarget.((*tcp.Target)))) + assert.Equal(targetAfterUpdate.GetAddress(), foundTarget.GetAddress()) underlyingDB, err := conn.SqlDB(ctx) require.NoError(err) dbassert := dbassert.New(t, underlyingDB) diff --git a/internal/target/tcp/target.go b/internal/target/tcp/target.go index fff99710b..b75d032ff 100644 --- a/internal/target/tcp/target.go +++ b/internal/target/tcp/target.go @@ -27,6 +27,8 @@ const ( // that can be accessed via TCP. It is a subtype of target.Target. type Target struct { *store.Target + // Network address assigned to the Target. + Address string `json:"address,omitempty" gorm:"-"` tableName string `gorm:"-"` } @@ -57,6 +59,7 @@ func (h targetHooks) NewTarget(projectId string, opt ...target.Option) (target.T EgressWorkerFilter: opts.WithEgressWorkerFilter, IngressWorkerFilter: opts.WithIngressWorkerFilter, }, + Address: opts.WithAddress, } return t, nil } @@ -72,7 +75,8 @@ func (h targetHooks) AllocTarget() target.Target { func (t *Target) Clone() target.Target { cp := proto.Clone(t.Target) return &Target{ - Target: cp.(*store.Target), + Target: cp.(*store.Target), + Address: t.Address, } } @@ -124,6 +128,10 @@ func (t *Target) GetType() subtypes.Subtype { return Subtype } +func (t *Target) GetAddress() string { + return t.Address +} + func (t *Target) SetPublicId(ctx context.Context, publicId string) error { const op = "tcp.(Target).SetPublicId" if !strings.HasPrefix(publicId, TargetPrefix+"_") { @@ -181,3 +189,7 @@ func (t *Target) SetEgressWorkerFilter(filter string) { func (t *Target) SetIngressWorkerFilter(filter string) { t.IngressWorkerFilter = filter } + +func (t *Target) SetAddress(address string) { + t.Address = address +} diff --git a/internal/target/tcp/target_test.go b/internal/target/tcp/target_test.go index c8c3b7c09..160d08dc4 100644 --- a/internal/target/tcp/target_test.go +++ b/internal/target/tcp/target_test.go @@ -324,7 +324,9 @@ func TestTarget_Clone(t *testing.T) { t.Run("valid", func(t *testing.T) { assert := assert.New(t) _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - tar := tcp.TestTarget(ctx, t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId)) + tar := tcp.TestTarget(ctx, t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId), + target.WithAddress("8.8.8.8"), + ) cp := tar.Clone() assert.True(proto.Equal(cp.(*tcp.Target).Target, tar.(*tcp.Target).Target)) }) diff --git a/internal/target/tcp/testing.go b/internal/target/tcp/testing.go index f127a4bc1..c7c527b11 100644 --- a/internal/target/tcp/testing.go +++ b/internal/target/tcp/testing.go @@ -26,6 +26,13 @@ func TestTarget(ctx context.Context, t testing.TB, conn *db.DB, projectId, name err = rw.Create(context.Background(), tar) require.NoError(err) + if opts.WithAddress != "" { + address, err := target.NewAddress(tar.GetPublicId(), opts.WithAddress) + require.NoError(err) + require.NotNil(address) + err = rw.Create(context.Background(), address) + require.NoError(err) + } if len(opts.WithHostSources) > 0 { newHostSets := make([]any, 0, len(opts.WithHostSources)) for _, s := range opts.WithHostSources { diff --git a/internal/target/testing.go b/internal/target/testing.go index ef5b36321..413f9e915 100644 --- a/internal/target/testing.go +++ b/internal/target/testing.go @@ -23,6 +23,17 @@ func TestNewCredentialLibrary(targetId, credentialLibraryId string, purpose cred } } +// TestNewTargetAddress creates a new in memory TargetAddress +// representing the association between a Target and network address. +func TestNewTargetAddress(targetId, address string) *Address { + return &Address{ + TargetAddress: &store.TargetAddress{ + TargetId: targetId, + Address: address, + }, + } +} + // TestNewStaticCredential creates a new in memory StaticCredential // representing the relationship between targetId and credentialId with // the given purpose. @@ -47,3 +58,15 @@ func TestCredentialLibrary(t testing.TB, conn *db.DB, targetId, libraryId string require.NoError(err) return lib } + +// TestTargetAddress creates am association for targetId and +// a given address value. +func TestTargetAddress(t testing.TB, conn *db.DB, targetId, address string) *Address { + t.Helper() + require := require.New(t) + rw := db.New(conn) + staticAddress := TestNewTargetAddress(targetId, address) + err := rw.Create(context.Background(), staticAddress) + require.NoError(err) + return staticAddress +} diff --git a/internal/tests/api/targets/target_test.go b/internal/tests/api/targets/target_test.go index 7046e0c03..f96734e6e 100644 --- a/internal/tests/api/targets/target_test.go +++ b/internal/tests/api/targets/target_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/api/credentials" "github.com/hashicorp/boundary/api/credentialstores" "github.com/hashicorp/boundary/api/hostcatalogs" + "github.com/hashicorp/boundary/api/hosts" "github.com/hashicorp/boundary/api/hostsets" "github.com/hashicorp/boundary/api/roles" "github.com/hashicorp/boundary/api/targets" @@ -379,6 +380,172 @@ func TestList(t *testing.T) { assert.Equal(filterItem.Id, ul.Items[0].Id) } +func TestTarget_AddressMutualExclusiveRelationship(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + client := tc.Client() + at := tc.Token() + client.SetToken(at.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(at.UserId)) + tClient := targets.NewClient(client) + + // Create target with a network address association + targetResp, err := tClient.Create(tc.Context(), "tcp", proj.GetPublicId(), + targets.WithName("test-address"), targets.WithAddress("localhost"), targets.WithTcpTargetDefaultPort(22)) + require.NoError(t, err) + require.NotNil(t, targetResp) + require.Equal(t, "localhost", targetResp.GetItem().Address) + + // Setup host catalog, host set, & host resources + hc, err := hostcatalogs.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, hc) + hs, err := hostsets.NewClient(client).Create(tc.Context(), hc.Item.Id) + require.NoError(t, err) + require.NotNil(t, hs) + h, err := hosts.NewClient(client).Create(tc.Context(), hc.Item.Id, hosts.WithStaticHostAddress("localhost")) + require.NoError(t, err) + require.NotNil(t, h) + hUpdate, err := hostsets.NewClient(client).AddHosts(tc.Context(), hs.Item.Id, hs.Item.Version, []string{h.GetItem().Id}) + require.NoError(t, err) + require.NotNil(t, hUpdate) + + // Expect error when associating a host source to the target with a direct network address association + targetId := targetResp.GetItem().Id + version := targetResp.GetItem().Version + updateResp, err := tClient.AddHostSources(tc.Context(), targetId, version, []string{hs.Item.Id}) + require.Error(t, err) + require.Nil(t, updateResp) + apiErr := api.AsServerError(err) + require.NotNil(t, apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.Response().StatusCode()) + + // Remove direct network address association. Successfully add a host source to target. + targetResp, err = tClient.Update(tc.Context(), targetId, version, targets.DefaultAddress()) + require.NoError(t, err) + require.NotNil(t, targetResp) + require.Empty(t, targetResp.GetItem().Address) + version = targetResp.GetItem().Version + updateResp, err = tClient.AddHostSources(tc.Context(), targetId, version, []string{hs.Item.Id}) + require.NoError(t, err) + require.NotNil(t, updateResp) + require.Empty(t, updateResp.GetItem().Address) + require.Equal(t, []string{hs.Item.Id}, updateResp.GetItem().HostSourceIds) +} + +func TestTarget_HostSourceMutualExclusiveRelationship(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + client := tc.Client() + at := tc.Token() + client.SetToken(at.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(at.UserId)) + tClient := targets.NewClient(client) + + // Setup host catalog, host set, & host resources + hc, err := hostcatalogs.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, hc) + hs, err := hostsets.NewClient(client).Create(tc.Context(), hc.Item.Id) + require.NoError(t, err) + require.NotNil(t, hs) + h, err := hosts.NewClient(client).Create(tc.Context(), hc.Item.Id, hosts.WithStaticHostAddress("localhost")) + require.NoError(t, err) + require.NotNil(t, h) + hUpdate, err := hostsets.NewClient(client).AddHosts(tc.Context(), hs.Item.Id, hs.Item.Version, []string{h.GetItem().Id}) + require.NoError(t, err) + require.NotNil(t, hUpdate) + + // Create target without a network address association + targetResp, err := tClient.Create(tc.Context(), "tcp", proj.GetPublicId(), + targets.WithName("test-host-source"), targets.WithTcpTargetDefaultPort(22)) + require.NoError(t, err) + require.NotNil(t, targetResp) + require.Empty(t, targetResp.GetItem().Address) + + // Expect error when associating a address to the target with a host source association + targetId := targetResp.GetItem().Id + version := targetResp.GetItem().Version + updateResp, err := tClient.AddHostSources(tc.Context(), targetId, version, []string{hs.Item.Id}) + require.NoError(t, err) + require.NotNil(t, updateResp) + require.Empty(t, updateResp.GetItem().Address) + require.Equal(t, []string{hs.Item.Id}, updateResp.GetItem().HostSourceIds) + version = updateResp.GetItem().Version + updateResp, err = tClient.Update(tc.Context(), targetId, version, targets.WithAddress("localhost")) + require.Error(t, err) + require.Nil(t, updateResp) + apiErr := api.AsServerError(err) + require.NotNil(t, apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.Response().StatusCode()) + + // Remove host source association. Successfully assign a network address to the target. + updateResp, err = tClient.RemoveHostSources(tc.Context(), targetId, version, []string{hs.Item.Id}) + require.NoError(t, err) + require.NotNil(t, updateResp) + require.Empty(t, updateResp.GetItem().HostSourceIds) + version = updateResp.GetItem().Version + updateResp, err = tClient.Update(tc.Context(), targetId, version, targets.WithAddress("localhost")) + require.NoError(t, err) + require.NotNil(t, updateResp) + require.Equal(t, "localhost", updateResp.GetItem().Address) + require.Empty(t, updateResp.GetItem().HostSourceIds) +} + +func TestCreateTarget_DirectlyAttachedAddress(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + client := tc.Client() + at := tc.Token() + client.SetToken(at.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(at.UserId)) + tClient := targets.NewClient(client) + + tests := []struct { + name string + address string + }{ + { + name: "target-ipv4-address", + address: "127.0.0.1", + }, + { + name: "target-dns-address", + address: "null", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + createResp, err := tClient.Create(tc.Context(), "tcp", proj.PublicId, + targets.WithName(tt.name), targets.WithAddress(tt.address), targets.WithTcpTargetDefaultPort(22)) + require.NoError(err) + require.NotNil(createResp) + assert.Equal(tt.address, createResp.GetItem().Address) + + targetId := createResp.GetItem().Id + version := createResp.GetItem().Version + readResp, err := tClient.Read(tc.Context(), targetId) + require.NoError(err) + require.NotNil(readResp) + assert.Equal(tt.address, readResp.GetItem().Address) + + updateResp, err := tClient.Update(tc.Context(), targetId, version, targets.DefaultAddress()) + require.NoError(err) + require.NotNil(updateResp) + assert.Empty(updateResp.GetItem().Address) + + readResp, err = tClient.Read(tc.Context(), targetId) + require.NoError(err) + require.NotNil(readResp) + assert.Empty(readResp.GetItem().Address) + }) + } +} + func comparableSlice(in []*targets.Target) []targets.Target { var filtered []targets.Target for _, i := range in { diff --git a/internal/tests/cli/boundary/_targets.bash b/internal/tests/cli/boundary/_targets.bash index 024b1afe4..5e7644d15 100644 --- a/internal/tests/cli/boundary/_targets.bash +++ b/internal/tests/cli/boundary/_targets.bash @@ -1,6 +1,7 @@ load _authorized_actions export TGT_NAME='test' +export TGT_NAME_WITH_ADDR='test-address' function create_tcp_target() { local sid=$1 @@ -13,6 +14,19 @@ function create_tcp_target() { -format json } +function create_tcp_target_with_addr() { + local sid=$1 + local addr=$2 + local port=$3 + local name=$4 + boundary targets create tcp \ + -address $addr \ + -default-port $port \ + -name $name \ + -scope-id $sid \ + -format json +} + function read_target() { boundary targets read -id $1 -format json } @@ -25,12 +39,24 @@ function list_targets() { boundary targets list -scope-id $1 -format json } +function update_address() { + local id=$1 + local addr=$2 + boundary targets update tcp -id $id -address $2 +} + function assoc_host_sources() { local id=$1 local hst=$2 boundary targets add-host-sources -id $id -host-source $hst } +function remove_host_sources() { + local id=$1 + local hst=$2 + boundary targets remove-host-sources -id $id -host-source $hst +} + function target_id_from_name() { local sid=$1 local name=$2 diff --git a/internal/tests/cli/boundary/target.bats b/internal/tests/cli/boundary/target.bats index 612d5d149..1f61772ac 100644 --- a/internal/tests/cli/boundary/target.bats +++ b/internal/tests/cli/boundary/target.bats @@ -100,3 +100,36 @@ load _helpers run read_target $id [ "$status" -eq 1 ] } + +@test "boundary/target: create target with a network address" { + run create_tcp_target_with_addr $DEFAULT_P_ID "localhost" 22 $TGT_NAME_WITH_ADDR + [ "$status" -eq 0 ] +} + +@test "boundary/target: cannot assign an host source to a target with an address" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME_WITH_ADDR) + run assoc_host_sources $id $DEFAULT_HOST_SET + [ "$status" -eq 1 ] +} + +@test "boundary/target: can assign an host source to a target after deleting address" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME_WITH_ADDR) + run update_address $id "null" + [ "$status" -eq 0 ] + run assoc_host_sources $id $DEFAULT_HOST_SET + [ "$status" -eq 0 ] +} + +@test "boundary/target: cannot assign an address to a target with an host source" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME_WITH_ADDR) + run update_address $id "localhost" + [ "$status" -eq 1 ] +} + +@test "boundary/target: can assign an an address to a target after deleting host source" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME_WITH_ADDR) + run remove_host_sources $id $DEFAULT_HOST_SET + [ "$status" -eq 0 ] + run update_address $id "localhost" + [ "$status" -eq 0 ] +} \ No newline at end of file diff --git a/sdk/pbs/controller/api/resources/targets/target.pb.go b/sdk/pbs/controller/api/resources/targets/target.pb.go index 3971ba0fc..cb395194a 100644 --- a/sdk/pbs/controller/api/resources/targets/target.pb.go +++ b/sdk/pbs/controller/api/resources/targets/target.pb.go @@ -372,6 +372,8 @@ type Target struct { Attrs isTarget_Attrs `protobuf_oneof:"attrs"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"` + // Optional string value that represents a network resource and is used when establishing a session. + Address *wrapperspb.StringValue `protobuf:"bytes,540,opt,name=address,proto3" json:"address,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *Target) Reset() { @@ -598,6 +600,13 @@ func (x *Target) GetAuthorizedActions() []string { return nil } +func (x *Target) GetAddress() *wrapperspb.StringValue { + if x != nil { + return x.Address + } + return nil +} + type isTarget_Attrs interface { isTarget_Attrs() } @@ -1234,7 +1243,7 @@ var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{ 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x52, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x22, 0xe8, 0x13, 0x0a, 0x06, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x22, 0xbd, 0x14, 0x0a, 0x06, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, @@ -1379,122 +1388,127 @@ var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{ 0x67, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, - 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x06, 0x08, 0x96, 0x01, 0x10, 0x97, 0x01, 0x4a, - 0x06, 0x08, 0xb4, 0x01, 0x10, 0xb5, 0x01, 0x4a, 0x06, 0x08, 0xf4, 0x03, 0x10, 0xf5, 0x03, 0x4a, - 0x06, 0x08, 0xfe, 0x03, 0x10, 0xff, 0x03, 0x4a, 0x04, 0x08, 0x64, 0x10, 0x65, 0x4a, 0x04, 0x08, - 0x6e, 0x10, 0x6f, 0x52, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, - 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x52, 0x20, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x52, 0x1c, 0x65, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x52, 0x19, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x52, 0x0c, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x73, - 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x22, 0x87, 0x01, 0x0a, 0x13, - 0x54, 0x63, 0x70, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, - 0x6f, 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, - 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, - 0x26, 0x0a, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x0b, 0x44, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x87, 0x01, 0x0a, 0x13, 0x53, 0x73, 0x68, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, - 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x2e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x26, 0x0a, 0x17, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, - 0x74, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, - 0x26, 0x0a, 0x0a, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xed, 0x03, 0x0a, 0x18, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x44, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, - 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, - 0x5a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x78, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0b, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, - 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x8c, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x8d, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x6e, - 0x66, 0x6f, 0x18, 0x96, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, - 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x22, 0xeb, 0x03, 0x0a, 0x14, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x43, - 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x32, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x12, 0x20, 0x0a, - 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x12, - 0x18, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, + 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x9c, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1a, + 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x12, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x06, 0x08, 0x96, + 0x01, 0x10, 0x97, 0x01, 0x4a, 0x06, 0x08, 0xb4, 0x01, 0x10, 0xb5, 0x01, 0x4a, 0x06, 0x08, 0xf4, + 0x03, 0x10, 0xf5, 0x03, 0x4a, 0x06, 0x08, 0xfe, 0x03, 0x10, 0xff, 0x03, 0x4a, 0x04, 0x08, 0x64, + 0x10, 0x65, 0x4a, 0x04, 0x08, 0x6e, 0x10, 0x6f, 0x52, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x52, 0x20, 0x61, 0x70, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x52, 0x1c, + 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x52, 0x19, 0x65, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x0c, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, + 0x74, 0x5f, 0x69, 0x64, 0x73, 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x73, + 0x22, 0x87, 0x01, 0x0a, 0x13, 0x54, 0x63, 0x70, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0xa0, 0xda, + 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x26, 0x0a, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x12, + 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x0c, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x87, 0x01, 0x0a, 0x13, 0x53, + 0x73, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, + 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x26, + 0x0a, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, + 0x70, 0x6f, 0x72, 0x74, 0x22, 0x26, 0x0a, 0x0a, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xed, 0x03, 0x0a, + 0x18, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x3e, 0x0a, 0x0c, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x20, 0x0a, 0x0b, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x78, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x21, + 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x82, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x8c, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x65, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x8d, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x0b, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x96, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x22, 0xeb, 0x03, 0x0a, + 0x14, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, + 0x74, 0x5f, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x58, 0x0a, 0x0b, 0x63, - 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x54, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x17, - 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x42, 0x50, 0x5a, 0x4e, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, - 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x73, 0x3b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x12, 0x58, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, + 0x6e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x0b, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x54, 0x0a, 0x1a, 0x55, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x22, 0x8c, 0x01, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, + 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x42, + 0x50, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x3b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1552,19 +1566,20 @@ var file_controller_api_resources_targets_v1_target_proto_depIdxs = []int32{ 12, // 18: controller.api.resources.targets.v1.Target.attributes:type_name -> google.protobuf.Struct 5, // 19: controller.api.resources.targets.v1.Target.tcp_target_attributes:type_name -> controller.api.resources.targets.v1.TcpTargetAttributes 6, // 20: controller.api.resources.targets.v1.Target.ssh_target_attributes:type_name -> controller.api.resources.targets.v1.SshTargetAttributes - 16, // 21: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 16, // 22: controller.api.resources.targets.v1.SshTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 13, // 23: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 15, // 24: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp - 7, // 25: controller.api.resources.targets.v1.SessionAuthorizationData.worker_info:type_name -> controller.api.resources.targets.v1.WorkerInfo - 13, // 26: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 15, // 27: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp - 3, // 28: controller.api.resources.targets.v1.SessionAuthorization.credentials:type_name -> controller.api.resources.targets.v1.SessionCredential - 29, // [29:29] is the sub-list for method output_type - 29, // [29:29] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 14, // 21: controller.api.resources.targets.v1.Target.address:type_name -> google.protobuf.StringValue + 16, // 22: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 16, // 23: controller.api.resources.targets.v1.SshTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 13, // 24: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 15, // 25: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp + 7, // 26: controller.api.resources.targets.v1.SessionAuthorizationData.worker_info:type_name -> controller.api.resources.targets.v1.WorkerInfo + 13, // 27: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 15, // 28: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp + 3, // 29: controller.api.resources.targets.v1.SessionAuthorization.credentials:type_name -> controller.api.resources.targets.v1.SessionCredential + 30, // [30:30] is the sub-list for method output_type + 30, // [30:30] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_controller_api_resources_targets_v1_target_proto_init() } diff --git a/sdk/pbs/controller/protooptions/options.pb.go b/sdk/pbs/controller/protooptions/options.pb.go index 2cf5c6542..293840ad2 100644 --- a/sdk/pbs/controller/protooptions/options.pb.go +++ b/sdk/pbs/controller/protooptions/options.pb.go @@ -129,7 +129,8 @@ var ( // optional controller.custom_options.v1.MaskMapping mask_mapping = 85464; E_MaskMapping = &file_controller_custom_options_v1_options_proto_extTypes[0] // generate_sdk_option is a directive used when generating the SDK to - // indicate that an option should be created for the field + // indicate that an option should be created for the field. This also controls + // API options (DefaultX, WithX functions). // // optional bool generate_sdk_option = 85412; E_GenerateSdkOption = &file_controller_custom_options_v1_options_proto_extTypes[1] diff --git a/testing/internal/e2e/boundary/target.go b/testing/internal/e2e/boundary/target.go index f7226af15..536641052 100644 --- a/testing/internal/e2e/boundary/target.go +++ b/testing/internal/e2e/boundary/target.go @@ -61,14 +61,64 @@ func CreateNewTargetCli(t testing.TB, ctx context.Context, projectId string, def return newTargetId } -// AddHostSourceToTargetCli uses the cli to add a host source (host set or host) to a target -func AddHostSourceToTargetCli(t testing.TB, ctx context.Context, targetId string, hostSourceId string) { +// CreateNewAddressTargetCli uses the cli to create a new target using an +// address in boundary. +// Returns the id of the new target. +func CreateNewAddressTargetCli(t testing.TB, ctx context.Context, projectId string, defaultPort string, address string) string { + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "create", "tcp", + "-scope-id", projectId, + "-default-port", defaultPort, + "-name", "e2e Target", + "-address", address, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newTargetResult targets.TargetCreateResult + err := json.Unmarshal(output.Stdout, &newTargetResult) + require.NoError(t, err) + newTargetId := newTargetResult.Item.Id + t.Logf("Created Target: %s", newTargetId) + + return newTargetId +} + +// AddHostSourceToTargetCli uses the cli to add a host source (host set or host) +// to a target. +// Boundary's `add-host-sources` functionality appends a new host source to the +// existing set of host sources in the target. +func AddHostSourceToTargetCli(t testing.TB, ctx context.Context, targetId, hostSourceId string) { output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("targets", "add-host-sources", "-id", targetId, "-host-source", hostSourceId), ) require.NoError(t, output.Err, string(output.Stderr)) } +// SetHostSourceToTargetCli uses the cli to set a host source (host set or host) +// to a target. +// Boundary's `set-host-sources` functionality replaces all existing host sets +// on a target with the provided one. +func SetHostSourceToTargetCli(t testing.TB, ctx context.Context, targetId, hostSourceId string) { + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("targets", "set-host-sources", "-id", targetId, "-host-source", hostSourceId), + ) + require.NoError(t, output.Err, string(output.Stderr)) +} + +// RemoveHostSourceFromTargetCli uses the cli to remove a host source (host set or host) to a target +func RemoveHostSourceFromTargetCli(t testing.TB, ctx context.Context, targetId, hostSourceId string) { + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "remove-host-sources", + "-id", targetId, + "-host-source", hostSourceId, + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) +} + // AddCredentialSourceToTargetCli uses the cli to add a credential source (credential library or // credential) to a target func AddCredentialSourceToTargetCli(t testing.TB, ctx context.Context, targetId string, credentialSourceId string) { diff --git a/testing/internal/e2e/tests/static/target_address_test.go b/testing/internal/e2e/tests/static/target_address_test.go new file mode 100644 index 000000000..b183358b2 --- /dev/null +++ b/testing/internal/e2e/tests/static/target_address_test.go @@ -0,0 +1,340 @@ +package static_test + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/require" +) + +// TestCliCreateUpdateTargetAddress asserts that creating and updating a target +// using a valid address is successful. +func TestCliCreateUpdateTargetAddress(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newProjectId := boundary.CreateNewProjectCli(t, ctx, newOrgId) + newTargetId := boundary.CreateNewAddressTargetCli(t, ctx, newProjectId, c.TargetPort, c.TargetIp) + + // Connect to target and print host's IP address + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + parts := strings.Fields(string(output.Stdout)) + hostIp := parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") + + // Remove target address. We have now no address or host sources. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "update", "tcp", + "-id", newTargetId, + "-address", "null", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Connect to target and print host's IP address - should error because + // there's no place to connect to. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-format", "json", + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.Error(t, output.Err, string(output.Stderr)) + + var response boundary.CliError + require.NoError(t, json.Unmarshal(output.Stderr, &response)) + require.Equal(t, http.StatusNotFound, response.Status, "Expected error when connecting to target with no host sources or address") + + // Add a valid address back in. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "update", "tcp", + "-id", newTargetId, + "-address", c.TargetIp, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Connect to target and print host's IP address. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + parts = strings.Fields(string(output.Stdout)) + hostIp = parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") +} + +// TestCliTargetAddressToHostSource asserts that we can change a target using an +// address to a target using host sources only if we remove the address first. +// TestCliTargetAddressToHostSource and TestCliTargetHostSourceToAddress are separate, +// independent tests because switching between host sources and address is not a +// commutative operation (they use different codepaths). +func TestCliTargetAddressToHostSource(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newProjectId := boundary.CreateNewProjectCli(t, ctx, newOrgId) + newHostCatalogId := boundary.CreateNewHostCatalogCli(t, ctx, newProjectId) + newHostSetId := boundary.CreateNewHostSetCli(t, ctx, newHostCatalogId) + newHostId := boundary.CreateNewHostCli(t, ctx, newHostCatalogId, c.TargetIp) + boundary.AddHostToHostSetCli(t, ctx, newHostSetId, newHostId) + newTargetId := boundary.CreateNewAddressTargetCli(t, ctx, newProjectId, c.TargetPort, c.TargetIp) + + // Connect to target and print host's IP address + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + parts := strings.Fields(string(output.Stdout)) + hostIp := parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") + + // Attempt add-host-sources. Should error because the target has an address. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "add-host-sources", + "-id", newTargetId, + "-host-source", newHostSetId, + "-format", "json", + ), + ) + require.Error(t, output.Err, "Target add-host-sources did not return expected error") + + var response boundary.CliError + require.NoError(t, json.Unmarshal(output.Stderr, &response)) + require.Equal(t, http.StatusBadRequest, response.Status, "Expected error when adding host source to target with an address") + + // Attempt set-host-sources. Should error because the target has an address. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "set-host-sources", + "-id", newTargetId, + "-host-source", newHostSetId, + "-format", "json", + ), + ) + require.Error(t, output.Err, "Target set-host-sources did not return expected error") + require.NoError(t, json.Unmarshal(output.Stderr, &response)) + require.Equal(t, http.StatusBadRequest, response.Status, "Expected error when setting host source to target with an address") + + // Remove the target's address. We should now be able to add/set host + // sources. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "update", "tcp", + "-id", newTargetId, + "-address", "null", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + boundary.AddHostSourceToTargetCli(t, ctx, newTargetId, newHostSetId) + boundary.SetHostSourceToTargetCli(t, ctx, newTargetId, newHostSetId) + + // Connect to target and print host's IP address, now using the host source. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + parts = strings.Fields(string(output.Stdout)) + hostIp = parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") +} + +// TestCliTargetHostSourceToAddress asserts that we can change a target using host +// source(s) to a target using an address only if we remove the host sources +// first. +// TestCliTargetAddressToHostSource and TestCliTargetHostSourceToAddress are separate, +// independent tests because switching between host sources and address is not a +// commutative operation (they use different codepaths). +func TestCliTargetHostSourceToAddress(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newProjectId := boundary.CreateNewProjectCli(t, ctx, newOrgId) + newHostCatalogId := boundary.CreateNewHostCatalogCli(t, ctx, newProjectId) + newHostSetId := boundary.CreateNewHostSetCli(t, ctx, newHostCatalogId) + newHostId := boundary.CreateNewHostCli(t, ctx, newHostCatalogId, c.TargetIp) + boundary.AddHostToHostSetCli(t, ctx, newHostSetId, newHostId) + newTargetId := boundary.CreateNewTargetCli(t, ctx, newProjectId, c.TargetPort) + boundary.AddHostSourceToTargetCli(t, ctx, newTargetId, newHostSetId) + + // Connect to target and print host's IP address + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + parts := strings.Fields(string(output.Stdout)) + hostIp := parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") + + // Attempt to add an address to the target - should error because we have + // host sources set. + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "update", "tcp", + "-id", newTargetId, + "-address", c.TargetIp, + "-format", "json", + ), + ) + require.Error(t, output.Err, "Target update did not return expected error") + + var response boundary.CliError + require.NoError(t, json.Unmarshal(output.Stderr, &response)) + require.Equal(t, http.StatusBadRequest, response.Status, "Expected error when setting address to target with a host source") + + boundary.RemoveHostSourceFromTargetCli(t, ctx, newTargetId, newHostSetId) + + // Attempt to add an address to the target again - should work + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "targets", "update", "tcp", + "-id", newTargetId, + "-address", c.TargetIp, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Connect to target and print host's IP address + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "connect", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) +} diff --git a/website/content/docs/api-clients/cli.mdx b/website/content/docs/api-clients/cli.mdx index 105f85270..7e05d57b7 100644 --- a/website/content/docs/api-clients/cli.mdx +++ b/website/content/docs/api-clients/cli.mdx @@ -130,8 +130,8 @@ However, a target can be one of many types of targets, and a concrete implementation of a target is a `tcp` type of target. Therefore an extra parameter is required when creating or updating a target: -- `boundary targets tcp create` -- `boundary targets tcp update` +- `boundary targets create tcp` +- `boundary targets update tcp` This allows the CLI to perform proper presentation and validation of parameters and functions for the given type. diff --git a/website/content/docs/concepts/domain-model/index.mdx b/website/content/docs/concepts/domain-model/index.mdx index 12c8ae84a..ac8d9cf3f 100644 --- a/website/content/docs/concepts/domain-model/index.mdx +++ b/website/content/docs/concepts/domain-model/index.mdx @@ -51,7 +51,29 @@ The highest-level in Boundary utilizes the concepts of [Roles](/docs/concepts/do ## Target and Host Resources -Boundary exposes endpoints to users as [Targets](/docs/concepts/domain-model/targets). A target is a resource representing a networked service, with an associated set of permissions, that allows a user to connect and interact with Boundary within a single session. A target may contain references to [Host](/docs/concepts/domain-model/hosts) sources and [Credential](/docs/concepts/domain-model/credentials) sources. Users who access a target create an authorized session to one of its host sources with returned credentials from the target’s credential source. +Boundary exposes endpoints to users as [Targets](/docs/concepts/domain-model/targets). A target is a resource representing a networked service, with an associated set of permissions, that allows a user to connect and interact with Boundary within a single session. A target may contain references to [Host](/docs/concepts/domain-model/hosts) sources or an address, and [Credential](/docs/concepts/domain-model/credentials) sources. Users who access a target create an authorized session to its address or one of its host sources with returned credentials from the target’s credential source. + +A target exposes two distinct and mutually exclusive configuration paths for its +representation of a networked service: + +- Target with an address +- Target with host sources + +A target with an address represents a target that is set up with a single IP +address or DNS name directly on the target resource. This mechanism provides +benefits for new Boundary users and users who do not need the flexibility +provided by Hosts, Host Sets, and Host Catalogs. It is well suited for cases +where there are stand-alone hosts that should not be grouped together and +treated as equal from an access control point of view. Because of its +simplicity, using targets with an address is not recommended when there are a +either a large number of hosts and/or dynamic hosts. + +A target with host sources represents a target that has one or more [host +sets][] associated with it. It trades off the simplicity of directly setting an +address on a target with the flexibility and scalability of Hosts, Host Sets and +Host Catalogs for discovering and categorizing networked resources. We recommend +using targets with host sources for setting up Boundary in environments where a +significant number of resources exist. ### Summary of Resources diff --git a/website/content/docs/concepts/domain-model/targets.mdx b/website/content/docs/concepts/domain-model/targets.mdx index 56dfa11eb..53db11871 100644 --- a/website/content/docs/concepts/domain-model/targets.mdx +++ b/website/content/docs/concepts/domain-model/targets.mdx @@ -19,11 +19,13 @@ A target can contain references to [host sets][] from [host catalogs][] which belong to the same project as the target. A target can contain references to [credential libraries][] from [credential stores][] which belong to the same project as the target. -A user can establish a session with a [host][] -in any host set referenced by the target -if the user has been assigned a [role][] -with the `authorize-session` [permission][] -for the target. +A target can contain an address +which is used by a session to connect to a networked resource. +A target cannot have an address and also reference host sources. +A user must be assigned a [role][] with the `authorize-session` [permission][] +for the target to +establish a session with a networked resource by way of an address, +or host in any host set referenced by the target. ## Attributes @@ -34,6 +36,10 @@ A target has the following configurable attributes: - `description` - (optional) +- `address` - (optional) + Represents a network resource address and is used when establishing a session. + Accepts no port, only an IP address or DNS name. + ### TCP Target Attributes TCP targets have the following additional attributes: diff --git a/website/content/docs/oss/installing/connect-to-dev-target.mdx b/website/content/docs/oss/installing/connect-to-dev-target.mdx index c6fe921af..b46eca454 100644 --- a/website/content/docs/oss/installing/connect-to-dev-target.mdx +++ b/website/content/docs/oss/installing/connect-to-dev-target.mdx @@ -8,16 +8,32 @@ description: |- ## Connect to Your First Target The default target is a `tcp` target with a default port of `22` (which -corresponds to the default SSH port using TCP). The host sets for this target -contain the default host, which has the address `127.0.0.1`. When `boundary -connect` is executed against this target, the single available host will be -selected and a local authenticated proxy to the host is established on the +corresponds to the default SSH port using TCP). This target is set up with +`127.0.0.1` as its address. When you execute `boundary connect` against this +target, a local authenticated proxy to the address is established on the target's default port (`127.0.0.1:22`). ```shell-session $ boundary connect -target-id ttcp_1234567890 ``` +There is also a secondary `tcp` target with (`ttcp_0987654321`) which uses host +sets instead of an address. The host sets for this target contain the default +host, which has the address 127.0.0.1. When you execute `boundary connect` +against this target, the single available host will be selected and a local +authenticated proxy to the host is also established on the target's default port +(127.0.0.1:22). + +```shell-session +boundary connect -target-id ttcp_0987654321 +``` + +For the purpose of this guide, both of these targets accomplish the same goal, +however they have different purposes. Refer to the +[domain](/docs/concepts/domain-model/index) documentation for more information. + +For the rest of this guide, assume the default is used. + The output displays the address and port that your SSH client must utilize. In the next section the `ssh` connect helper is used to make it easier to connect to the target with a client. @@ -130,6 +146,7 @@ of `:22` to `:443`: $ boundary targets update tcp -default-port 443 -id ttcp_1234567890 Target information: + Address: 127.0.0.1 Created Time: Fri, 25 Sep 2020 18:35:47 PDT Description: Provides an initial target in Boundary ID: ttcp_1234567890 @@ -146,10 +163,6 @@ Target information: Parent Scope ID: o_1234567890 Type: project - Host Sets: - Host Catalog ID: hcst_1234567890 - ID: hsst_1234567890 - Attributes: Default Port: 443 ``` diff --git a/website/content/docs/oss/installing/dev-mode.mdx b/website/content/docs/oss/installing/dev-mode.mdx index da5493acd..70f0c28fa 100644 --- a/website/content/docs/oss/installing/dev-mode.mdx +++ b/website/content/docs/oss/installing/dev-mode.mdx @@ -42,7 +42,9 @@ connect to your first target: set](/docs/concepts/domain-model/host-sets) with default ID's of `hst_1234567890` and `hsst_1234567890` respectively. 1. A TCP [target](/docs/concepts/domain-model/targets) with a default ID of - `ttcp_1234567890`. + `ttcp_1234567890`, directly set-up with an address and port. +1. A TCP [target](/docs/concepts/domain-model/targets) with a default ID of + `ttcp_0987654321` using the previously mentioned static host set. The default ID suffixes can be overwritten or randomly generated, and there are many other dev mode controls. To see a complete list of these override flags,