diff --git a/internal/cmd/base/initial_resources.go b/internal/cmd/base/initial_resources.go index 06ba317bd..a4b430471 100644 --- a/internal/cmd/base/initial_resources.go +++ b/internal/cmd/base/initial_resources.go @@ -48,7 +48,7 @@ func (b *Server) CreateInitialLoginRole(ctx context.Context) (*iam.Role, error) pr, err := iam.NewRole(scope.Global.String(), iam.WithName("Login and Default Grants"), - iam.WithDescription(`Role created for login capability and account self-management for users of the global scope at its creation time`), + iam.WithDescription(`Role created for login capability, account self-management, and other default grants for users of the global scope at its creation time`), ) if err != nil { return nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) @@ -125,78 +125,110 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet b.InfoKeys = append(b.InfoKeys, "generated auth method id") b.Info["generated auth method id"] = b.DevAuthMethodId - // Create the dev user - if b.DevLoginName == "" { - b.DevLoginName, err = base62.Random(10) - if err != nil { - return nil, nil, fmt.Errorf("unable to generate login name: %w", err) + createUser := func(loginName, loginPassword, userId string, admin bool) (*iam.User, error) { + // Create the dev admin user + if loginName == "" { + b.DevLoginName, err = base62.Random(10) + if err != nil { + return nil, fmt.Errorf("unable to generate login name: %w", err) + } + loginName = strings.ToLower(b.DevLoginName) } - b.DevLoginName = strings.ToLower(b.DevLoginName) - } - if b.DevPassword == "" { - b.DevPassword, err = base62.Random(20) - if err != nil { - return nil, nil, fmt.Errorf("unable to generate password: %w", err) + if b.DevPassword == "" { + b.DevPassword, err = base62.Random(20) + if err != nil { + return nil, fmt.Errorf("unable to generate password: %w", err) + } + loginPassword = b.DevPassword } - } - b.InfoKeys = append(b.InfoKeys, "generated auth method password") - b.Info["generated auth method password"] = b.DevPassword + typeStr := "admin" + if !admin { + typeStr = "unprivileged" + } + b.InfoKeys = append(b.InfoKeys, fmt.Sprintf("generated %s password", typeStr)) + b.Info[fmt.Sprintf("generated %s password", typeStr)] = loginPassword - acct, err := password.NewAccount(b.DevAuthMethodId, password.WithLoginName(b.DevLoginName)) - if err != nil { - return nil, nil, fmt.Errorf("error creating new in memory auth account: %w", err) - } - acct, err = pwRepo.CreateAccount(cancelCtx, scope.Global.String(), acct, password.WithPassword(b.DevPassword)) - if err != nil { - return nil, nil, fmt.Errorf("error saving auth account to the db: %w", err) - } - b.InfoKeys = append(b.InfoKeys, "generated auth method login name") - b.Info["generated auth method login name"] = acct.GetLoginName() + acct, err := password.NewAccount(am.PublicId, password.WithLoginName(loginName)) + if err != nil { + return nil, fmt.Errorf("error creating new in memory auth account: %w", err) + } + acct, err = pwRepo.CreateAccount(cancelCtx, scope.Global.String(), acct, password.WithPassword(loginPassword)) + if err != nil { + return nil, fmt.Errorf("error saving auth account to the db: %w", err) + } + b.InfoKeys = append(b.InfoKeys, fmt.Sprintf("generated %s login name", typeStr)) + b.Info[fmt.Sprintf("generated %s login name", typeStr)] = acct.GetLoginName() - iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) - if err != nil { - return nil, nil, fmt.Errorf("unable to create repo for org id: %w", err) + iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) + if err != nil { + return nil, fmt.Errorf("unable to create repo for org id: %w", err) + } + + // Create a new user and associate it with the account + if userId == "" { + userId, err = db.NewPublicId(iam.UserPrefix) + if err != nil { + return nil, fmt.Errorf("error generating initial user id: %w", err) + } + } + opts := []iam.Option{ + iam.WithPublicId(userId), + } + if admin { + opts = append(opts, + iam.WithName("admin"), + iam.WithDescription(`Initial admin user within the "global" scope`), + ) + } else { + opts = append(opts, + iam.WithName("user"), + iam.WithDescription("Initial unprivileged user"), + ) + } + u, err := iam.NewUser(scope.Global.String(), opts...) + if err != nil { + return nil, fmt.Errorf("error creating in memory user: %w", err) + } + if u, err = iamRepo.CreateUser(cancelCtx, u, opts...); err != nil { + return nil, fmt.Errorf("error creating initial %s user: %w", typeStr, err) + } + if _, err = iamRepo.AddUserAccounts(cancelCtx, u.GetPublicId(), u.GetVersion(), []string{acct.GetPublicId()}); err != nil { + return nil, fmt.Errorf("error associating initial %s user with account: %w", typeStr, err) + } + if !admin { + return u, nil + } + // Create a role tying them together + pr, err := iam.NewRole(scope.Global.String(), + iam.WithName("Administration"), + iam.WithDescription(`Provides admin grants within the "global" scope to the initial user`), + ) + if err != nil { + return nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) + } + defPermsRole, err := iamRepo.CreateRole(cancelCtx, pr) + if err != nil { + return nil, fmt.Errorf("error creating role for default generated grants: %w", err) + } + if _, err := iamRepo.AddRoleGrants(cancelCtx, defPermsRole.PublicId, defPermsRole.Version, []string{"id=*;type=*;actions=*"}); err != nil { + return nil, fmt.Errorf("error creating grant for default generated grants: %w", err) + } + if _, err := iamRepo.AddPrincipalRoles(cancelCtx, defPermsRole.PublicId, defPermsRole.Version+1, []string{u.GetPublicId()}, nil); err != nil { + return nil, fmt.Errorf("error adding principal to role for default generated grants: %w", err) + } + return u, nil } - // Create a new user and associate it with the account - if b.DevUserId == "" { - b.DevUserId, err = db.NewPublicId(iam.UserPrefix) + if b.DevUnprivilegedLoginName != "" { + unprivUser, err := createUser(b.DevUnprivilegedLoginName, b.DevUnprivilegedPassword, b.DevUnprivilegedUserId, false) if err != nil { - return nil, nil, fmt.Errorf("error generating initial user id: %w", err) + return nil, nil, err } + b.DevUnprivilegedUserId = unprivUser.GetPublicId() } - opts := []iam.Option{ - iam.WithName("admin"), - iam.WithDescription(`Initial admin user within the "global" scope`), - iam.WithPublicId(b.DevUserId), - } - u, err := iam.NewUser(scope.Global.String(), opts...) + u, err := createUser(b.DevLoginName, b.DevPassword, b.DevUserId, true) if err != nil { - return nil, nil, fmt.Errorf("error creating in memory user: %w", err) - } - if u, err = iamRepo.CreateUser(cancelCtx, u, opts...); err != nil { - return nil, nil, fmt.Errorf("error creating initial admin user: %w", err) - } - if _, err = iamRepo.AddUserAccounts(cancelCtx, u.GetPublicId(), u.GetVersion(), []string{acct.GetPublicId()}); err != nil { - return nil, nil, fmt.Errorf("error associating initial admin user with account: %w", err) - } - // Create a role tying them together - pr, err := iam.NewRole(scope.Global.String(), - iam.WithName("Administration"), - iam.WithDescription(`Provides admin grants within the "global" scope to the initial user`), - ) - if err != nil { - return nil, nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) - } - defPermsRole, err := iamRepo.CreateRole(cancelCtx, pr) - if err != nil { - return nil, nil, fmt.Errorf("error creating role for default generated grants: %w", err) - } - if _, err := iamRepo.AddRoleGrants(cancelCtx, defPermsRole.PublicId, defPermsRole.Version, []string{"id=*;type=*;actions=*"}); err != nil { - return nil, nil, fmt.Errorf("error creating grant for default generated grants: %w", err) - } - if _, err := iamRepo.AddPrincipalRoles(cancelCtx, defPermsRole.PublicId, defPermsRole.Version+1, []string{u.GetPublicId()}, nil); err != nil { - return nil, nil, fmt.Errorf("error adding principal to role for default generated grants: %w", err) + return nil, nil, err } return am, u, nil @@ -456,5 +488,64 @@ func (b *Server) CreateInitialTarget(ctx context.Context) (target.Target, error) b.InfoKeys = append(b.InfoKeys, "generated target id") b.Info["generated target id"] = b.DevTargetId + // If we have an unprivileged dev user, add user to the role that grants + // list/read:self/cancel:self, 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) + } + + 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 { + panic("") + 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`), + ) + if err != nil { + return nil, fmt.Errorf("error creating in memory role for generated grants: %w", err) + } + sessionRole, err := iamRepo.CreateRole(cancelCtx, pr) + if err != nil { + return nil, fmt.Errorf("error creating role for unprivileged user generated grants: %w", err) + } + if _, err := iamRepo.AddRoleGrants(cancelCtx, + 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(cancelCtx, 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 tt, nil } diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index b77635725..8da50e5bf 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -71,6 +71,9 @@ type Server struct { DevLoginName string DevPassword string DevUserId string + DevUnprivilegedLoginName string + DevUnprivilegedPassword string + DevUnprivilegedUserId string DevOrgId string DevProjectId string DevHostCatalogId string diff --git a/internal/cmd/commands/accounts/account.go b/internal/cmd/commands/accounts/account.go index 17378ae99..06bd1d2de 100644 --- a/internal/cmd/commands/accounts/account.go +++ b/internal/cmd/commands/accounts/account.go @@ -218,6 +218,17 @@ func (c *Command) Run(args []string) int { c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) return 2 } + fmt.Print("Please enter it one more time for confirmation: ") + confirmation, err := password.Read(os.Stdin) + fmt.Print("\n") + if err != nil { + c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) + return 2 + } + if strings.TrimSpace(value) != strings.TrimSpace(confirmation) { + c.UI.Error("Entered password and confirmation value did not match.") + return 2 + } c.flagPassword = strings.TrimSpace(value) } @@ -240,6 +251,17 @@ func (c *Command) Run(args []string) int { c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) return 2 } + fmt.Print("Please enter it one more time for confirmation: ") + confirmation, err := password.Read(os.Stdin) + fmt.Print("\n") + if err != nil { + c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) + return 2 + } + if strings.TrimSpace(value) != strings.TrimSpace(confirmation) { + c.UI.Error("Entered password and confirmation value did not match.") + return 2 + } c.flagNewPassword = strings.TrimSpace(value) } diff --git a/internal/cmd/commands/accounts/password.go b/internal/cmd/commands/accounts/password.go index d12681155..e5bb3a6e6 100644 --- a/internal/cmd/commands/accounts/password.go +++ b/internal/cmd/commands/accounts/password.go @@ -172,6 +172,17 @@ func (c *PasswordCommand) Run(args []string) int { c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) return 2 } + fmt.Print("Please enter it one more time for confirmation: ") + confirmation, err := password.Read(os.Stdin) + fmt.Print("\n") + if err != nil { + c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) + return 2 + } + if strings.TrimSpace(value) != strings.TrimSpace(confirmation) { + c.UI.Error("Entered password and confirmation value did not match.") + return 2 + } opts = append(opts, accounts.WithPasswordAccountPassword(strings.TrimSpace(value))) default: opts = append(opts, accounts.WithPasswordAccountPassword(c.flagPassword)) diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index 0b89591d8..19a7c6911 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/servers/worker" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/sdk/strutil" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -43,6 +44,8 @@ type Command struct { flagCombineLogs bool flagLoginName string flagPassword string + flagUnprivilegedLoginName string + flagUnprivilegedPassword string flagIdSuffix string flagHostAddress string flagTargetDefaultPort int @@ -111,7 +114,7 @@ func (c *Command) Flags() *base.FlagSets { Target: &c.flagPassword, Default: "password", EnvVar: "BOUNDARY_DEV_PASSWORD", - Usage: "Initial login password. If set to the empty string, one will be autogenerated.", + Usage: "Initial admin login password. If set to the empty string, one will be autogenerated.", }) f.StringVar(&base.StringVar{ @@ -122,6 +125,22 @@ func (c *Command) Flags() *base.FlagSets { Usage: "Initial admin login name. If set to the empty string, one will be autogenerated.", }) + f.StringVar(&base.StringVar{ + Name: "unprivileged-password", + Target: &c.flagUnprivilegedPassword, + Default: "password", + EnvVar: "BOUNDARY_DEV_UNPRIVILEGED_PASSWORD", + Usage: "Initial unprivileged user login password. If set to the empty string, one will be autogenerated.", + }) + + f.StringVar(&base.StringVar{ + Name: "unprivileged-login-name", + Target: &c.flagUnprivilegedLoginName, + Default: "user", + EnvVar: "BOUNDARY_DEV_UNPRIVILEGED_LOGIN_NAME", + Usage: "Initial unprivileged user name. If set to the empty string, one will be autogenerated.", + }) + f.StringVar(&base.StringVar{ Name: "api-listen-address", Target: &c.flagControllerAPIListenAddr, @@ -261,6 +280,7 @@ func (c *Command) Run(args []string) int { } c.DevAuthMethodId = fmt.Sprintf("%s_%s", password.AuthMethodPrefix, c.flagIdSuffix) c.DevUserId = fmt.Sprintf("%s_%s", iam.UserPrefix, c.flagIdSuffix) + c.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(c.DevUserId, "u_")) c.DevOrgId = fmt.Sprintf("%s_%s", scope.Org.Prefix(), c.flagIdSuffix) c.DevProjectId = fmt.Sprintf("%s_%s", scope.Project.Prefix(), c.flagIdSuffix) c.DevHostCatalogId = fmt.Sprintf("%s_%s", static.HostCatalogPrefix, c.flagIdSuffix) @@ -274,6 +294,12 @@ func (c *Command) Run(args []string) int { if c.flagPassword != "" { c.DevPassword = c.flagPassword } + if c.flagUnprivilegedLoginName != "" { + c.DevUnprivilegedLoginName = c.flagUnprivilegedLoginName + } + if c.flagUnprivilegedPassword != "" { + c.DevUnprivilegedPassword = c.flagUnprivilegedPassword + } c.DevTargetDefaultPort = c.flagTargetDefaultPort host, port, err := net.SplitHostPort(c.flagHostAddress) if err != nil { diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index ddcd659f5..444f9222c 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -126,7 +126,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o var defaultRoleMetadata oplog.Metadata var defaultRole *Role var defaultRoleRaw interface{} - if !opts.withSkipDefaultRoleCreation && s.Type == scope.Org.String() { + if !opts.withSkipDefaultRoleCreation { defaultRole, err = NewRole(scopePublicId) if err != nil { return nil, errors.Wrap(err, op, errors.WithMsg("error instantiating new default role")) @@ -136,8 +136,14 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o return nil, errors.Wrap(err, op, errors.WithMsg("error generating public id for new default role")) } defaultRole.PublicId = defaultRolePublicId - defaultRole.Name = "Login and Default Grants" - defaultRole.Description = fmt.Sprintf("Role created for login capability and account self-management for users of scope %s at its creation time", scopePublicId) + switch s.Type { + case scope.Project.String(): + defaultRole.Name = "Default Grants" + defaultRole.Description = fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", scopePublicId) + default: + defaultRole.Name = "Login and Default Grants" + defaultRole.Description = fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId) + } defaultRoleRaw = defaultRole defaultRoleMetadata = oplog.Metadata{ "resource-public-id": []string{defaultRolePublicId}, @@ -280,23 +286,34 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o // Grants { grants := []interface{}{} - roleGrant, err := NewRoleGrant(defaultRolePublicId, "id=*;type=scope;actions=list,read") - if err != nil { - return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) - } - grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "id=*;type=auth-method;actions=authenticate,list") - if err != nil { - return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) - } - grants = append(grants, roleGrant) + switch s.Type { + case scope.Project.String(): + roleGrant, err := NewRoleGrant(defaultRolePublicId, "id=*;type=session;actions=list,read:self,cancel:self") + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) + } + grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "id={{account.id}};actions=read,change-password") - if err != nil { - return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) + default: + roleGrant, err := NewRoleGrant(defaultRolePublicId, "id=*;type=scope;actions=list,read") + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) + } + grants = append(grants, roleGrant) + + roleGrant, err = NewRoleGrant(defaultRolePublicId, "id=*;type=auth-method;actions=authenticate,list") + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) + } + grants = append(grants, roleGrant) + + roleGrant, err = NewRoleGrant(defaultRolePublicId, "id={{account.id}};actions=read,change-password") + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant")) + } + grants = append(grants, roleGrant) } - grants = append(grants, roleGrant) roleGrantOplogMsgs := make([]*oplog.Message, 0, 3) if err := w.CreateItems(ctx, grants, db.NewOplogMsgs(&roleGrantOplogMsgs)); err != nil { @@ -308,7 +325,11 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o // Principals { principals := []interface{}{} - rolePrincipal, err := NewUserRole(defaultRolePublicId, "u_anon") + userId := "u_anon" + if s.Type == scope.Project.String() { + userId = "u_auth" + } + rolePrincipal, err := NewUserRole(defaultRolePublicId, userId) if err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role user")) } diff --git a/internal/servers/controller/handlers/scopes/scope_service_test.go b/internal/servers/controller/handlers/scopes/scope_service_test.go index 5069ba52b..7356bba32 100644 --- a/internal/servers/controller/handlers/scopes/scope_service_test.go +++ b/internal/servers/controller/handlers/scopes/scope_service_test.go @@ -717,7 +717,7 @@ func TestCreate(t *testing.T) { require.NoError(err) switch tc.scopeId { case defaultOrg.PublicId: - require.Len(roles, 1) + require.Len(roles, 2) case "global": require.Len(roles, 2) } @@ -726,7 +726,9 @@ func TestCreate(t *testing.T) { case "Administration": assert.Equal(fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", got.GetItem().GetId(), userId), role.GetDescription()) case "Login and Default Grants": - assert.Equal(fmt.Sprintf("Role created for login capability and account self-management for users of scope %s at its creation time", got.GetItem().GetId()), role.GetDescription()) + assert.Equal(fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", got.GetItem().GetId()), role.GetDescription()) + case "Default Grants": + assert.Equal(fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", got.GetItem().GetId()), role.GetDescription()) default: t.Fatal("unexpected role name", role.GetName()) } diff --git a/internal/servers/controller/handlers/sessions/session_service.go b/internal/servers/controller/handlers/sessions/session_service.go index e29ab422f..25304b25a 100644 --- a/internal/servers/controller/handlers/sessions/session_service.go +++ b/internal/servers/controller/handlers/sessions/session_service.go @@ -25,7 +25,9 @@ var ( // individual resources IdActions = action.ActionSet{ action.Read, + action.ReadSelf, action.Cancel, + action.CancelSelf, } // CollectionActions contains the set of actions that can be performed on @@ -61,7 +63,7 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p if err := validateGetRequest(req); err != nil { return nil, err } - authResults := s.authResult(ctx, req.GetId(), action.Read) + authResults := s.authResult(ctx, req.GetId(), action.ReadSelf) if authResults.Error != nil { return nil, authResults.Error } @@ -69,8 +71,24 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p if err != nil { return nil, err } + + authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions) + // Check to see if we need to verify Read vs. just ReadSelf + if ses.GetUserId() != authResults.UserId { + var found bool + for _, v := range authzdActions { + if v == action.Read { + found = true + break + } + } + if !found { + return nil, handlers.ForbiddenError() + } + } + ses.Scope = authResults.Scope - ses.AuthorizedActions = authResults.FetchActionSetForId(ctx, ses.Id, IdActions).Strings() + ses.AuthorizedActions = authzdActions.Strings() return &pbs.GetSessionResponse{Item: ses}, nil } @@ -101,9 +119,19 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest) for _, item := range seslist { item.Scope = scopeInfoMap[item.GetScopeId()] res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) > 0 { - finalItems = append(finalItems, item) + authorizedActions := authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)) + if len(authorizedActions) > 0 { + onlySelf := true + for _, v := range authorizedActions { + if v != action.ReadSelf && v != action.CancelSelf { + onlySelf = false + break + } + } + if !onlySelf || item.GetUserId() == authResults.UserId { + item.AuthorizedActions = authorizedActions.Strings() + finalItems = append(finalItems, item) + } } } @@ -115,16 +143,36 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques if err := validateCancelRequest(req); err != nil { return nil, err } - authResults := s.authResult(ctx, req.GetId(), action.Cancel) + authResults := s.authResult(ctx, req.GetId(), action.CancelSelf) if authResults.Error != nil { return nil, authResults.Error } - ses, err := s.cancelInRepo(ctx, req.GetId(), req.GetVersion()) + + ses, err := s.getFromRepo(ctx, req.GetId()) + if err != nil { + return nil, err + } + authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions) + // Check to see if we need to verify Read vs. just ReadSelf + if ses.GetUserId() != authResults.UserId { + var found bool + for _, v := range authzdActions { + if v == action.Cancel { + found = true + break + } + } + if !found { + return nil, handlers.ForbiddenError() + } + } + + ses, err = s.cancelInRepo(ctx, req.GetId(), req.GetVersion()) if err != nil { return nil, err } ses.Scope = authResults.Scope - ses.AuthorizedActions = authResults.FetchActionSetForId(ctx, ses.Id, IdActions).Strings() + ses.AuthorizedActions = authzdActions.Strings() return &pbs.CancelSessionResponse{Item: ses}, nil } @@ -196,7 +244,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. res.Error = handlers.NotFoundError() return res } - case action.Read, action.Cancel: + case action.Read, action.ReadSelf, action.Cancel, action.CancelSelf: repo, err := s.repoFn() if err != nil { res.Error = err diff --git a/internal/servers/controller/handlers/sessions/session_service_test.go b/internal/servers/controller/handlers/sessions/session_service_test.go index edee282ed..a6be5d0da 100644 --- a/internal/servers/controller/handlers/sessions/session_service_test.go +++ b/internal/servers/controller/handlers/sessions/session_service_test.go @@ -81,7 +81,7 @@ func TestGetSession(t *testing.T) { States: []*pb.SessionState{{Status: session.StatusPending.String(), StartTime: sess.CreateTime.GetTimestamp()}}, Certificate: sess.Certificate, Type: target.TcpSubType.String(), - AuthorizedActions: []string{"read", "cancel"}, + AuthorizedActions: []string{"read", "read:self", "cancel", "cancel:self"}, } cases := []struct { @@ -210,7 +210,7 @@ func TestList(t *testing.T) { States: states, Certificate: sess.Certificate, Type: target.TcpSubType.String(), - AuthorizedActions: []string{"read", "cancel"}, + AuthorizedActions: []string{"read", "read:self", "cancel", "cancel:self"}, }) totalSession = append(totalSession, wantSession[i]) @@ -245,7 +245,7 @@ func TestList(t *testing.T) { States: states, Certificate: sess.Certificate, Type: target.TcpSubType.String(), - AuthorizedActions: []string{"read", "cancel"}, + AuthorizedActions: []string{"read", "read:self", "cancel", "cancel:self"}, }) } @@ -362,7 +362,7 @@ func TestCancel(t *testing.T) { Status: session.StatusCanceling.String(), Certificate: sess.Certificate, Type: target.TcpSubType.String(), - AuthorizedActions: []string{"read", "cancel"}, + AuthorizedActions: []string{"read", "read:self", "cancel", "cancel:self"}, } version := wireSess.GetVersion() diff --git a/internal/servers/controller/multi_test.go b/internal/servers/controller/multi_test.go index 454bf2e31..f727f54b2 100644 --- a/internal/servers/controller/multi_test.go +++ b/internal/servers/controller/multi_test.go @@ -15,18 +15,12 @@ import ( func TestAuthenticationMulti(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - user := "user" - password := "passpass" logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, }) c1 := controller.NewTestController(t, &controller.TestControllerOpts{ - DefaultAuthMethodId: amId, - DefaultLoginName: user, - DefaultPassword: password, - Logger: logger.Named("c1"), + Logger: logger.Named("c1"), }) defer c1.Shutdown() @@ -36,14 +30,14 @@ func TestAuthenticationMulti(t *testing.T) { defer c2.Shutdown() auth := authmethods.NewClient(c1.Client()) - token1Result, err := auth.Authenticate(c1.Context(), amId, map[string]interface{}{"login_name": user, "password": password}) + token1Result, err := auth.Authenticate(c1.Context(), c1.Server().DevAuthMethodId, map[string]interface{}{"login_name": c1.Server().DevLoginName, "password": c1.Server().DevPassword}) require.Nil(err) token1 := token1Result.Item require.NotNil(token1) time.Sleep(5 * time.Second) auth = authmethods.NewClient(c2.Client()) - token2Result, err := auth.Authenticate(c2.Context(), amId, map[string]interface{}{"login_name": user, "password": password}) + token2Result, err := auth.Authenticate(c2.Context(), c2.Server().DevAuthMethodId, map[string]interface{}{"login_name": c2.Server().DevLoginName, "password": c2.Server().DevPassword}) require.Nil(err) token2 := token2Result.Item require.NotNil(token2) diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index e39c6cd25..72dd4b1ab 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/sdk/strutil" "github.com/hashicorp/go-hclog" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/vault/sdk/helper/base62" @@ -25,9 +26,11 @@ import ( ) const ( - DefaultTestAuthMethodId = "ampw_1234567890" - DefaultTestLoginName = "user" - DefaultTestPassword = "passpass" + DefaultTestAuthMethodId = "ampw_1234567890" + DefaultTestLoginName = "admin" + DefaultTestUnprivilegedLoginName = "user" + DefaultTestPassword = "passpass" + DefaultTestUserId = "u_1234567890" ) // TestController wraps a base.Server and Controller to provide a @@ -46,6 +49,11 @@ type TestController struct { opts *TestControllerOpts } +// Server returns the underlying base server +func (tc *TestController) Server() *base.Server { + return tc.b +} + // Controller returns the underlying controller func (tc *TestController) Controller() *Controller { return tc.c @@ -135,6 +143,26 @@ func (tc *TestController) Token() *authtokens.AuthToken { return token.Item } +func (tc *TestController) UnprivilegedToken() *authtokens.AuthToken { + if tc.opts.DisableAuthMethodCreation { + tc.t.Error("no default auth method ID configured") + return nil + } + token, err := authmethods.NewClient(tc.Client()).Authenticate( + tc.Context(), + tc.b.DevAuthMethodId, + map[string]interface{}{ + "login_name": tc.b.DevUnprivilegedLoginName, + "password": tc.b.DevUnprivilegedPassword, + }, + ) + if err != nil { + tc.t.Error(fmt.Errorf("error logging in: %w", err)) + return nil + } + return token.Item +} + func (tc *TestController) addrs(purpose string) []string { var prefix string switch purpose { @@ -242,10 +270,13 @@ type TestControllerOpts struct { // DefaultAuthMethodId is the default auth method ID to use, if set. DefaultAuthMethodId string - // DefaultLoginName is the login name used when creating the default account. + // DefaultLoginName is the login name used when creating the default admin account. DefaultLoginName string - // DefaultPassword is the password used when creating the default account. + // DefaultUnprivilegedLoginName is the login name used when creating the default unprivileged account. + DefaultUnprivilegedLoginName string + + // DefaultPassword is the password used when creating the default accounts. DefaultPassword string // DisableInitialLoginRoleCreation can be set true to disable creating the @@ -360,10 +391,17 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { } else { tc.b.DevLoginName = DefaultTestLoginName } + if opts.DefaultUnprivilegedLoginName != "" { + tc.b.DevUnprivilegedLoginName = opts.DefaultUnprivilegedLoginName + } else { + tc.b.DevUnprivilegedLoginName = DefaultTestUnprivilegedLoginName + } if opts.DefaultPassword != "" { tc.b.DevPassword = opts.DefaultPassword + tc.b.DevUnprivilegedPassword = opts.DefaultPassword } else { tc.b.DevPassword = DefaultTestPassword + tc.b.DevUnprivilegedPassword = DefaultTestPassword } // Start a logger @@ -396,7 +434,11 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { tc.b.DevProjectId = "p_" + suffix tc.b.DevTargetId = "ttcp_" + suffix tc.b.DevUserId = "u_" + suffix + tc.b.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(tc.b.DevUserId, "u_")) + } else { + tc.b.DevUserId = DefaultTestUserId } + tc.b.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(tc.b.DevUserId, "u_")) // Set up KMSes switch { @@ -509,6 +551,8 @@ func (tc *TestController) AddClusterControllerMember(t *testing.T, opts *TestCon RecoveryKms: tc.c.conf.RecoveryKms, Name: opts.Name, Logger: tc.c.conf.Logger, + DefaultLoginName: tc.b.DevLoginName, + DefaultPassword: tc.b.DevPassword, DisableKmsKeyCreation: true, DisableAuthMethodCreation: true, } diff --git a/internal/tests/api/accounts/account_test.go b/internal/tests/api/accounts/account_test.go index 10d1b8e36..6d71e48f5 100644 --- a/internal/tests/api/accounts/account_test.go +++ b/internal/tests/api/accounts/account_test.go @@ -3,7 +3,6 @@ package accounts_test import ( "fmt" "net/http" - "os" "testing" "github.com/hashicorp/boundary/api" @@ -17,8 +16,6 @@ import ( ) func TestList(t *testing.T) { - os.Setenv("BOUNDARY_LOG_URLS", "1") - os.Setenv("BOUNDARY_DEV_SKIP_AUTHZ", "1") assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) defer tc.Shutdown() @@ -126,23 +123,37 @@ func TestCustomMethods(t *testing.T) { client.SetToken(token.Token) amId := token.AuthMethodId - accountClient := accounts.NewClient(client) + adminAccountClient := accounts.NewClient(client) + client = client.Clone() + client.SetToken(tc.UnprivilegedToken().Token) + userAccountClient := accounts.NewClient(client) - al, err := accountClient.List(tc.Context(), amId) + al, err := adminAccountClient.List(tc.Context(), amId) require.NoError(err) - require.Len(al.Items, 1) + require.Len(al.Items, 2) - acct := al.Items[0] + userAcct := al.Items[0] + if userAcct.Name == "admin" { + userAcct = al.Items[1] + } - setAcct, err := accountClient.SetPassword(tc.Context(), acct.Id, "setpassword", acct.Version) + _, err = userAccountClient.SetPassword(tc.Context(), userAcct.Id, "setpassword", userAcct.Version) + require.Error(err) + + setAcct, err := adminAccountClient.SetPassword(tc.Context(), userAcct.Id, "setpassword", userAcct.Version) require.NoError(err) require.NotNil(setAcct) - assert.Equal(acct.Version+1, setAcct.Item.Version) + assert.Equal(userAcct.Version+1, setAcct.Item.Version) - changeAcct, err := accountClient.ChangePassword(tc.Context(), acct.Id, "setpassword", "changepassword", setAcct.Item.Version) + changeAcct, err := adminAccountClient.ChangePassword(tc.Context(), userAcct.Id, "setpassword", "changepassword", setAcct.Item.Version) require.NoError(err) require.NotNil(changeAcct) - assert.Equal(setAcct.Item.Version+1, changeAcct.Item.Version) + assert.Equal(userAcct.Version+2, changeAcct.Item.Version) + + changeAcct, err = userAccountClient.ChangePassword(tc.Context(), userAcct.Id, "changepassword", "password2", changeAcct.Item.Version) + require.NoError(err) + require.NotNil(changeAcct) + assert.Equal(userAcct.Version+3, changeAcct.Item.Version) } func TestErrors(t *testing.T) { diff --git a/internal/tests/api/authmethods/authenticate_test.go b/internal/tests/api/authmethods/authenticate_test.go index 5fc3dce8a..d5f73cdba 100644 --- a/internal/tests/api/authmethods/authenticate_test.go +++ b/internal/tests/api/authmethods/authenticate_test.go @@ -13,22 +13,17 @@ import ( func TestAuthenticate(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - tc := controller.NewTestController(t, &controller.TestControllerOpts{ - DefaultAuthMethodId: amId, - DefaultLoginName: "user", - DefaultPassword: "passpass", - }) + tc := controller.NewTestController(t, nil) defer tc.Shutdown() client := tc.Client() methods := authmethods.NewClient(client) - tok, err := methods.Authenticate(tc.Context(), amId, map[string]interface{}{"login_name": "user", "password": "passpass"}) + tok, err := methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) assert.NotNil(tok) - _, err = methods.Authenticate(tc.Context(), amId, map[string]interface{}{"login_name": "user", "password": "wrong"}) + _, err = methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, map[string]interface{}{"login_name": "user", "password": "wrong"}) require.Error(err) apiErr := api.AsServerError(err) require.NotNil(apiErr) diff --git a/internal/tests/api/authtokens/authtoken_test.go b/internal/tests/api/authtokens/authtoken_test.go index ca1ee7e24..f15f3e4ec 100644 --- a/internal/tests/api/authtokens/authtoken_test.go +++ b/internal/tests/api/authtokens/authtoken_test.go @@ -19,12 +19,7 @@ import ( func TestList(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - tc := controller.NewTestController(t, &controller.TestControllerOpts{ - DefaultAuthMethodId: amId, - DefaultLoginName: "user", - DefaultPassword: "passpass", - }) + tc := controller.NewTestController(t, nil) defer tc.Shutdown() token := tc.Token() @@ -36,7 +31,7 @@ func TestList(t *testing.T) { amResult, err := amClient.Create(tc.Context(), "password", org.GetPublicId()) require.NoError(err) require.NotNil(amResult) - amId = amResult.Item.Id + amId := amResult.Item.Id rolesClient := roles.NewClient(client) role, err := rolesClient.Create(tc.Context(), org.GetPublicId()) @@ -106,12 +101,7 @@ func comparableSlice(in []*authtokens.AuthToken) []authtokens.AuthToken { func TestCrud(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - tc := controller.NewTestController(t, &controller.TestControllerOpts{ - DefaultAuthMethodId: amId, - DefaultLoginName: "user", - DefaultPassword: "passpass", - }) + tc := controller.NewTestController(t, nil) defer tc.Shutdown() client := tc.Client() @@ -120,7 +110,7 @@ func TestCrud(t *testing.T) { tokens := authtokens.NewClient(client) methods := authmethods.NewClient(client) - want, err := methods.Authenticate(tc.Context(), amId, map[string]interface{}{"login_name": "user", "password": "passpass"}) + want, err := methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) at, err := tokens.Read(tc.Context(), want.Item.Id) diff --git a/internal/tests/api/roles/role_test.go b/internal/tests/api/roles/role_test.go index 6a1872269..23e24d2b9 100644 --- a/internal/tests/api/roles/role_test.go +++ b/internal/tests/api/roles/role_test.go @@ -107,7 +107,6 @@ func TestCustom(t *testing.T) { require.NoError(err) assert.EqualValues(updatedRole.Item.Version, version) assert.Empty(updatedRole.Item.Grants) - version++ }) } } @@ -148,7 +147,7 @@ func TestList(t *testing.T) { case "org": numBuiltIn = 2 case "proj": - numBuiltIn = 1 + numBuiltIn = 2 } require.Len(p1.Items, numBuiltIn) expected = append(expected, p1.Items[0:numBuiltIn]...) diff --git a/internal/tests/cli/boundary/_helpers.bash b/internal/tests/cli/boundary/_helpers.bash index a79886a74..ec87630ae 100644 --- a/internal/tests/cli/boundary/_helpers.bash +++ b/internal/tests/cli/boundary/_helpers.bash @@ -1,6 +1,7 @@ export BOUNDARY_ADDR="${BOUNDARY_ADDR:-http://127.0.0.1:9200}" export DEFAULT_PASSWORD="${DEFAULT_PASSWORD:-password}" export DEFAULT_LOGIN="${DEFAULT_LOGIN:-admin}" +export DEFAULT_UNPRIVILEGED_LOGIN="${DEFAULT_UNPRIVILEGED_LOGIN:-user}" export DEFAULT_AMPW="${DEFAULT_AMPW:-ampw_1234567890}" export DEFAULT_P_ID='p_1234567890' export DEFAULT_O_ID='o_1234567890' @@ -10,6 +11,7 @@ export DEFAULT_HOST_SET='hsst_1234567890' export DEFAULT_HOST_CATALOG='hcst_1234567890' export DEFAULT_HOST='hst_1234567890' export DEFAULT_USER='u_1234567890' +export DEFAULT_UNPRIVILEGED_USER='u_0987654321' export DEFAULT_GLOBAL='global' function strip() { diff --git a/internal/tests/cli/boundary/_sessions.bash b/internal/tests/cli/boundary/_sessions.bash new file mode 100644 index 000000000..1d1b7b7d4 --- /dev/null +++ b/internal/tests/cli/boundary/_sessions.bash @@ -0,0 +1,17 @@ +load _authorized_actions + +function list_sessions() { + boundary sessions list -scope-id $1 -format json +} + +function count_sessions() { + list_sessions $1 | jq '. | length' +} + +function cancel_session() { + boundary sessions cancel -id $1 +} + +function read_session() { + boundary sessions read -id $1 +} \ No newline at end of file diff --git a/internal/tests/cli/boundary/_targets.bash b/internal/tests/cli/boundary/_targets.bash index b0bd79792..05c5b1ca2 100644 --- a/internal/tests/cli/boundary/_targets.bash +++ b/internal/tests/cli/boundary/_targets.bash @@ -31,7 +31,7 @@ function assoc_host_sets() { boundary targets add-host-sets -id $id -host-set $hst } -function target_id() { +function target_id_from_name() { local sid=$1 local name=$2 strip $(list_targets $sid | jq -c ".[] | select(.name | contains(\"$name\")) | .[\"id\"]") diff --git a/internal/tests/cli/boundary/sessions.bats b/internal/tests/cli/boundary/sessions.bats new file mode 100644 index 000000000..33d12d1bb --- /dev/null +++ b/internal/tests/cli/boundary/sessions.bats @@ -0,0 +1,104 @@ +#!/usr/bin/env bats + +load _auth +load _connect +load _sessions +load _helpers + +@test "boundary/session: admin user can connect to default target" { + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] + + run connect_nc $DEFAULT_TARGET + echo "$output" + [ "$status" -eq 0 ] + + # Run twice so we have two values for later testing + run connect_nc $DEFAULT_TARGET + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/session/connect: unpriv user can connect to default target" { + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] + + run connect_nc $DEFAULT_TARGET + echo "$output" + [ "$status" -eq 0 ] + + # Run twice so we have two values for later testing + run connect_nc $DEFAULT_TARGET + echo "$output" + [ "$status" -eq 0 ] +} + +# Note: there seems to be an issue with jq's length calculation -- an increase +# in the returned array by 2 shows up as a 4 or more increase in jq's length count. +# So for now, verify they're not the same. +@test "boundary/session: verify admin and unpriv user see different counts" { + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] + + run count_sessions $DEFAULT_P_ID + [ "$status" -eq 0 ] + unpriv_sessions="$output" + + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] + run count_sessions $DEFAULT_P_ID + [ "$status" -eq 0 ] + admin_sessions="$output" + + [ "$unpriv_sessions" -lt "$admin_sessions" ] +} + +@test "boundary/session: verify read and cancelation permissions on admin session" { + # Find an admin session + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] + run list_sessions $DEFAULT_P_ID + [ "$status" -eq 0 ] + id=$(echo "$output" | jq -r '[.[]|select(.user_id == "u_1234567890")][0].id') + + # Check unpriv cannot read or cancel an admin's session + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] + run read_session $id + [ "$status" -eq 1 ] + run cancel_session $id + [ "$status" -eq 1 ] + + # Check that admin _can_ + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] + run read_session $id + [ "$status" -eq 0 ] + run cancel_session $id + [ "$status" -eq 0 ] +} + +@test "boundary/session: verify read and cancelation permissions on unpriv session" { + # Find an unpriv session + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] + run list_sessions $DEFAULT_P_ID + [ "$status" -eq 0 ] + id=$(echo "$output" | jq -r '[.[]|select(.user_id == "u_0987654321")][0].id') + + # Check unpriv can read and cancel their own session + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] + run read_session $id + [ "$status" -eq 0 ] + run cancel_session $id + [ "$status" -eq 0 ] + + # Check that admin can too + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] + run read_session $id + [ "$status" -eq 0 ] + run cancel_session $id + [ "$status" -eq 0 ] +} \ No newline at end of file diff --git a/internal/tests/cli/boundary/target.bats b/internal/tests/cli/boundary/target.bats index 0f18d95f8..f53f2d4be 100644 --- a/internal/tests/cli/boundary/target.bats +++ b/internal/tests/cli/boundary/target.bats @@ -6,35 +6,61 @@ load _targets load _helpers -@test "boundary/login: can login as default user" { +@test "boundary/login: can login as admin user" { run login $DEFAULT_LOGIN [ "$status" -eq 0 ] } -@test "boundary/target/connect: default user can connect to default target" { +@test "boundary/target/connect: admin user can connect to default target" { run connect_nc $DEFAULT_TARGET echo "$output" [ "$status" -eq 0 ] } -@test "boundary/target: default user can create target" { +@test "boundary/target: admin user can read default target" { + run read_target $DEFAULT_TARGET + [ "$status" -eq 0 ] +} + +@test "boundary/login: can login as unpriv user" { + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] +} + +@test "boundary/target/connect: unpriv user can connect to default target" { + run connect_nc $DEFAULT_TARGET + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/target: unpriv user can not read default target" { + run read_target $DEFAULT_TARGET + [ "$status" -eq 1 ] +} + +@test "boundary/login: login back in as admin user" { + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] +} + +@test "boundary/target: admin user can create target" { run create_tcp_target $DEFAULT_P_ID 22 $TGT_NAME [ "$status" -eq 0 ] } -@test "boundary/target: default user can not create already created target" { +@test "boundary/target: admin user can not create already created target" { run create_tcp_target $DEFAULT_P_ID 22 $TGT_NAME [ "$status" -eq 1 ] } -@test "boundary/target: default user can read created target" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) +@test "boundary/target: admin user can read created target" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run read_target $id [ "$status" -eq 0 ] } @test "boundary/target: the $TGT_NAME target contains default authorized-actions" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) local out=$(read_target $id) run has_default_target_actions "$out" @@ -42,35 +68,35 @@ load _helpers [ "$status" -eq 0 ] } -@test "boundary/target: default user can add default host set to created target" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) +@test "boundary/target: admin user can add default host set to created target" { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run assoc_host_sets $id $DEFAULT_HOST_SET echo "$output" [ "$status" -eq 0 ] } @test "boundary/target: created target has default host set" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run target_has_host_set_id $id $DEFAULT_HOST_SET echo "$output" [ "$status" -eq 0 ] } @test "boundary/target/connect: default user can connect to created target" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run connect_nc $id echo "connecting to $id: $output" [ "$status" -eq 0 ] } @test "boundary/target: default user can delete target" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run delete_target $id [ "$status" -eq 0 ] } @test "boundary/target: default user can not read deleted target" { - local id=$(target_id $DEFAULT_P_ID $TGT_NAME) + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run read_target $id [ "$status" -eq 1 ] } diff --git a/internal/tests/cluster/ipv6_listener_test.go b/internal/tests/cluster/ipv6_listener_test.go index 936f1103d..421c37bae 100644 --- a/internal/tests/cluster/ipv6_listener_test.go +++ b/internal/tests/cluster/ipv6_listener_test.go @@ -17,9 +17,6 @@ import ( func TestIPv6Listener(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - user := "user" - password := "passpass" logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, }) @@ -38,11 +35,8 @@ func TestIPv6Listener(t *testing.T) { } c1 := controller.NewTestController(t, &controller.TestControllerOpts{ - Config: conf, - DefaultAuthMethodId: amId, - DefaultLoginName: user, - DefaultPassword: password, - Logger: logger.Named("c1"), + Config: conf, + Logger: logger.Named("c1"), }) defer c1.Shutdown() diff --git a/internal/tests/cluster/multi_controller_worker_test.go b/internal/tests/cluster/multi_controller_worker_test.go index 63d3c6672..272e5a0b4 100644 --- a/internal/tests/cluster/multi_controller_worker_test.go +++ b/internal/tests/cluster/multi_controller_worker_test.go @@ -13,9 +13,6 @@ import ( func TestMultiControllerMultiWorkerConnections(t *testing.T) { require := require.New(t) - amId := "ampw_1234567890" - user := "user" - password := "passpass" logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, }) @@ -24,11 +21,8 @@ func TestMultiControllerMultiWorkerConnections(t *testing.T) { require.NoError(err) c1 := controller.NewTestController(t, &controller.TestControllerOpts{ - Config: conf, - DefaultAuthMethodId: amId, - DefaultLoginName: user, - DefaultPassword: password, - Logger: logger.Named("c1"), + Config: conf, + Logger: logger.Named("c1"), }) defer c1.Shutdown() diff --git a/internal/tests/cluster/unix_listener_test.go b/internal/tests/cluster/unix_listener_test.go index 2885a740c..92d3101ed 100644 --- a/internal/tests/cluster/unix_listener_test.go +++ b/internal/tests/cluster/unix_listener_test.go @@ -20,9 +20,6 @@ import ( func TestUnixListener(t *testing.T) { assert, require := assert.New(t), require.New(t) - amId := "ampw_1234567890" - user := "user" - password := "passpass" logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, }) @@ -50,11 +47,8 @@ func TestUnixListener(t *testing.T) { } c1 := controller.NewTestController(t, &controller.TestControllerOpts{ - Config: conf, - DefaultAuthMethodId: amId, - DefaultLoginName: user, - DefaultPassword: password, - Logger: logger.Named("c1"), + Config: conf, + Logger: logger.Named("c1"), }) defer c1.Shutdown() diff --git a/internal/tests/cluster/worker_tagging_test.go b/internal/tests/cluster/worker_tagging_test.go index 104fcc1da..31851eb6b 100644 --- a/internal/tests/cluster/worker_tagging_test.go +++ b/internal/tests/cluster/worker_tagging_test.go @@ -17,9 +17,6 @@ import ( ) func TestWorkerTagging(t *testing.T) { - amId := "ampw_1234567890" - user := "user" - password := "passpass" logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, }) @@ -29,9 +26,6 @@ func TestWorkerTagging(t *testing.T) { c1 := controller.NewTestController(t, &controller.TestControllerOpts{ Config: conf, - DefaultAuthMethodId: amId, - DefaultLoginName: user, - DefaultPassword: password, InitialResourcesSuffix: "1234567890", Logger: logger.Named("c1"), }) diff --git a/internal/website/permstable/permstable.go b/internal/website/permstable/permstable.go index 9c72eff12..548dc83c0 100644 --- a/internal/website/permstable/permstable.go +++ b/internal/website/permstable/permstable.go @@ -689,14 +689,14 @@ var session = &Resource{ }, { Name: "read:self", - Description: "Read a session, which must belong to the calling user", + Description: "Read a session, which must be associated with the calling user", Examples: []string{ "id=*;type=session;actions=read:self", }, }, { Name: "cancel:self", - Description: "Cancel a session, which must belong to the calling user", + Description: "Cancel a session, which must be associated with the calling user", Examples: []string{ "id=*;type=session;actions=cancel:self", }, diff --git a/sdk/strutil/strutil.go b/sdk/strutil/strutil.go index 16b3a8598..2186680b6 100644 --- a/sdk/strutil/strutil.go +++ b/sdk/strutil/strutil.go @@ -464,3 +464,13 @@ func StringListToInterfaceList(in []string) []interface{} { } return ret } + +// Reverse reverses the input string +func Reverse(in string) string { + l := len(in) + out := make([]byte, l) + for i := 0; i <= l/2; i++ { + out[i], out[l-1-i] = in[l-1-i], in[i] + } + return string(out) +} diff --git a/sdk/strutil/strutil_test.go b/sdk/strutil/strutil_test.go index 977213150..6257e948f 100644 --- a/sdk/strutil/strutil_test.go +++ b/sdk/strutil/strutil_test.go @@ -633,3 +633,25 @@ func TestPrintable(t *testing.T) { }) } } + +func TestReverse(t *testing.T) { + cases := []struct { + in string + out string + }{ + { + in: "abc", + out: "cba", + }, + { + in: "abcd", + out: "dcba", + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprint(i), func(t *testing.T) { + require.Equal(t, Reverse(tc.in), tc.out) + }) + } +} diff --git a/website/content/docs/concepts/security/permissions.mdx b/website/content/docs/concepts/security/permissions.mdx index 5e10f2813..1e9dcb446 100644 --- a/website/content/docs/concepts/security/permissions.mdx +++ b/website/content/docs/concepts/security/permissions.mdx @@ -28,6 +28,22 @@ Roles are composable; a user's final set of grants will be composed of various roles that each contribute grants to a set of principals that include that user or groups of which that user is a member. +## Subactions + +Starting in Boundary 0.1.6, some subactions are supported. These actions have a +format `top_level_action:subaction`, such as `read:self`. Being granted the top +level action infers being granted all subactions. Thus, if a grant conveys +`read`, it also matches the API actions `read` and `read:self`. However, if a +grant conveys `read:self`, it will match the API action `read:self` but will not +match `read`. + +Note that not all API subactions are available for grants. As an example, auth +methods require one or more steps to perform authentication. These are separated +into separate API actions based on the required workflow for that authentication +mechanism, e.g. `authenticate:login`. However, the grant system does not +currently expose subactions for the `authenticate` top level action, as it +generally does not make sense to allow only one step out of many. + ## Applicable Resource Types Boundary's [domain model](/docs/concepts/domain-model) is based on resource @@ -1251,8 +1267,8 @@ wildcard or templated grant strings.
  • - read:self: Read a session, which must belong to the - calling user + read:self: Read a session, which must be associated + with the calling user
  • - cancel:self: Cancel a session, which must belong to the - calling user + cancel:self: Cancel a session, which must be associated + with the calling user