From f85da3ee2b5aa5daf8a607b81b50cbbede888c67 Mon Sep 17 00:00:00 2001 From: Hugo Vieira Date: Tue, 20 Dec 2022 20:51:56 +0000 Subject: [PATCH] feat(cmd): Create target using address on boundary dev / database init Creates a target using an address for boundary dev and when first init-ing boundary (boundary database init). It also makes this newly created target the default one (ttcp_1234567890). The target using host sources is now a secondary target (ttcp_0987654321). --- internal/cmd/base/dev.go | 5 +- internal/cmd/base/initial_resources.go | 228 +++++++++++++----- internal/cmd/base/servers.go | 6 +- internal/cmd/commands/database/init.go | 38 ++- internal/cmd/commands/dev/dev.go | 28 ++- .../commands/server/listener_reload_test.go | 2 +- internal/cmd/commands/server/server_test.go | 14 +- .../server/worker_shutdown_reload_test.go | 3 +- internal/daemon/controller/testing.go | 2 +- 9 files changed, 236 insertions(+), 90 deletions(-) diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index 90d702ad09..3581fee6da 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 c9c7aef064..c0e6c522ec 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(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("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.DevTargetId) == 0 { + b.DevTargetId, err = db.NewPublicId(tcp.TargetPrefix) + if err != nil { + 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 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("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) + } + b.InfoKeys = append(b.InfoKeys, "generated target with address id") + b.Info["generated target with address id"] = b.DevTargetId + + if b.DevUnprivilegedUserId != "" { + iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) + if err != nil { + 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) + } + } + + return tt, nil +} + +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("error adding config keys to kms: %w", err) + 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 == "" { - b.DevTargetId, err = db.NewPublicId(tcp.TargetPrefix) + // 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 generating initial target id: %w", err) + return nil, fmt.Errorf("failed to generate initial secondary target id: %w", err) } } if b.DevTargetDefaultPort == 0 { b.DevTargetDefaultPort = 22 } + opts := []target.Option{ - target.WithName("Generated target"), - target.WithDescription("Provides an initial target in Boundary"), + 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.DevTargetId), + target.WithPublicId(b.DevSecondaryTargetId), } 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) + return nil, fmt.Errorf("failed to add host source %q to target: %w", b.DevHostSetId, err) } - b.InfoKeys = append(b.InfoKeys, "generated target id") - b.Info["generated target id"] = b.DevTargetId + b.InfoKeys = append(b.InfoKeys, "generated target with host source id") + b.Info["generated target with host source id"] = b.DevSecondaryTargetId - // 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) } - - 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) - } - - 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`), - ) + err = unprivilegedDevUserRoleSetup(ctx, iamRepo, b.DevUnprivilegedUserId, b.DevProjectId, b.DevSecondaryTargetId) if err != nil { - return nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) - } - sessionRole, err := iamRepo.CreateRole(ctx, pr) - if err != nil { - return nil, fmt.Errorf("error creating role for unprivileged user generated grants: %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) + 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 14afcf016f..ff9f5dc8ef 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/database/init.go b/internal/cmd/commands/database/init.go index 1c83407fe1..b02ee3ec35 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 7193106c0a..b4ce7e3fb6 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 27fa5efa44..9e40c47cb0 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 2454f09a2c..de1d9a9400 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 773cedfe11..d99556d2ac 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/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 34f5de30fb..6df00fb476 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) } }