Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v10] Connect: Fetch correct role set when listing db users (#13790) #13877

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,38 @@ func FetchRoles(roleNames []string, access RoleGetter, traits map[string][]strin
return NewRoleSet(roles...), nil
}

// CurrentUserRoleGetter limits the interface of auth.ClientI to methods needed by FetchClusterRoles.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// CurrentUserRoleGetter limits the interface of auth.ClientI to methods needed by FetchClusterRoles.
// CurrentUserRoleGetter limits the interface of auth.ClientI to methods needed by FetchAllClusterRoles.

nit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll push a PR for that tomorrow.

type CurrentUserRoleGetter interface {
GetCurrentUser(context.Context) (types.User, error)
RoleGetter
}

// FetchAllClusterRoles fetches all roles available to the user on the specified cluster.
func FetchAllClusterRoles(ctx context.Context, access CurrentUserRoleGetter, defaultRoles []string, defaultTraits wrappers.Traits) (RoleSet, error) {
roles := defaultRoles
traits := defaultTraits

// Typically, auth.ClientI is passed as currentUserRoleGetter. Older versions of the auth client
// may not implement GetCurrentUser() so we fail gracefully and use default roles and traits instead.
user, err := access.GetCurrentUser(ctx)
if err == nil {
roles = user.GetRoles()
traits = user.GetTraits()
} else {
log.Debugf("Failed to fetch current user information: %v.", err)
}

// get the role definition for all roles of user.
// this may only fail if the role which we are looking for does not exist, or we don't have access to it.
// example scenario when this may happen:
// 1. we have set of roles [foo bar] from profile.
// 2. the cluster is remote and maps the [foo, bar] roles to single role [guest]
// 3. the remote cluster doesn't implement GetCurrentUser(), so we have no way to learn of [guest].
// 4. FetchRoles([foo bar], ..., ...) fails as [foo bar] does not exist on remote cluster.
roleSet, err := FetchRoles(roles, access, traits)
return roleSet, trace.Wrap(err)
}

// ExtractRolesFromCert extracts roles from certificate metadata extensions.
func ExtractRolesFromCert(cert *ssh.Certificate) ([]string, error) {
data, ok := cert.Extensions[teleport.CertExtensionTeleportRoles]
Expand Down
104 changes: 104 additions & 0 deletions lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package services

import (
"bytes"
"context"
"encoding/json"
"fmt"
"strconv"
Expand Down Expand Up @@ -4937,3 +4938,106 @@ func TestHostUsers_CanCreateHostUser(t *testing.T) {
})
}
}

type mockCurrentUserRoleGetter struct {
currentUser types.User
nameToRole map[string]types.Role
}

func (m mockCurrentUserRoleGetter) GetCurrentUser(ctx context.Context) (types.User, error) {
if m.currentUser != nil {
return m.currentUser, nil
}
return nil, trace.NotFound("currentUser not set")
}

func (m mockCurrentUserRoleGetter) GetRole(ctx context.Context, name string) (types.Role, error) {
if role, ok := m.nameToRole[name]; ok {
return role, nil
}
return nil, trace.NotFound("role not found: %v", name)
}

type mockCurrentUser struct {
types.User
roles []string
traits wrappers.Traits
}

func (u mockCurrentUser) GetRoles() []string {
return u.roles
}

func (u mockCurrentUser) GetTraits() map[string][]string {
return u.traits
}

func TestFetchAllClusterRoles_PrefersRolesAndTraitsFromCurrentUser(t *testing.T) {
defaultRoles := []string{"access", "editor"}
defaultTraits := map[string][]string{
"logins": {"defaultTraitLogin"},
}

user := mockCurrentUser{
roles: []string{"dev", "admin"},
traits: map[string][]string{
"logins": {"currentUserTraitLogin"},
},
}

devRole := newRole(func(r *types.RoleV5) {
r.Metadata.Name = "dev"
r.Spec.Allow.Logins = []string{"{{internal.logins}}"}
})
adminRole := newRole(func(r *types.RoleV5) {
r.Metadata.Name = "admin"
})

currentUserRoleGetter := mockCurrentUserRoleGetter{
nameToRole: map[string]types.Role{
"dev": &devRole,
"admin": &adminRole,
},
currentUser: user,
}

roleSet, err := FetchAllClusterRoles(context.Background(), currentUserRoleGetter,
defaultRoles, defaultTraits)

require.NoError(t, err)

require.Contains(t, roleSet, &devRole, "devRole not found in roleSet")
require.Contains(t, roleSet, &adminRole, "adminRole not found in roleSet")
require.Equal(t, []string{"currentUserTraitLogin"}, roleSet[0].GetLogins(types.Allow))
}

func TestFetchAllClusterRoles_UsesDefaultRolesAndTraitsIfCurrentUserIsUnavailable(t *testing.T) {
defaultRoles := []string{"access", "editor"}
defaultTraits := map[string][]string{
"logins": {"defaultTraitLogin"},
}

accessRole := newRole(func(r *types.RoleV5) {
r.Metadata.Name = "access"
r.Spec.Allow.Logins = []string{"{{internal.logins}}"}
})
editorRole := newRole(func(r *types.RoleV5) {
r.Metadata.Name = "editor"
})

currentUserRoleGetter := mockCurrentUserRoleGetter{
nameToRole: map[string]types.Role{
"access": &accessRole,
"editor": &editorRole,
},
}

roleSet, err := FetchAllClusterRoles(context.Background(), currentUserRoleGetter,
defaultRoles, defaultTraits)

require.NoError(t, err)

require.Contains(t, roleSet, &accessRole, "accessRole not found in roleSet")
require.Contains(t, roleSet, &editorRole, "editorRole not found in roleSet")
require.Equal(t, []string{"defaultTraitLogin"}, roleSet[0].GetLogins(types.Allow))
}
2 changes: 1 addition & 1 deletion lib/teleterm/clusters/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type Cluster struct {
Name string

// Log is a component logger
Log logrus.FieldLogger
Log *logrus.Entry
// dir is the directory where cluster certificates are stored
dir string
// Status is the cluster status
Expand Down
27 changes: 22 additions & 5 deletions lib/teleterm/clusters/cluster_databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
dbprofile "github.com/gravitational/teleport/lib/client/db"
libdefaults "github.com/gravitational/teleport/lib/defaults"
Expand Down Expand Up @@ -152,17 +153,33 @@ func (c *Cluster) ReissueDBCerts(ctx context.Context, user, dbName string, db ty

// GetAllowedDatabaseUsers returns allowed users for the given database based on the role set.
func (c *Cluster) GetAllowedDatabaseUsers(ctx context.Context, dbURI string) ([]string, error) {
var roleSet services.RoleSet
var err error
var authClient auth.ClientI

err = addMetadataToRetryableError(ctx, func() error {
roleSet, err = services.FetchRoles(c.status.Roles, c.clusterClient, c.status.Traits)
return err
err := addMetadataToRetryableError(ctx, func() error {
proxyClient, err := c.clusterClient.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()

authClient, err = proxyClient.ConnectToCluster(ctx, c.clusterClient.SiteName)
if err != nil {
return trace.Wrap(err)
}

return nil
})
if err != nil {
return nil, trace.Wrap(err)
}

defer authClient.Close()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a nil check here, or is authClient guaranteed to be initialized if we got this far?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is guaranteed to be initialized? If the function passed to addMetadataToRetryableError returns an error, the execution stops at line 173. The only way for that function to return nil is after the execution has succeeded and authClient is equal to the return value of proxyClient.ConnectToCluster.


roleSet, err := services.FetchAllClusterRoles(ctx, authClient, c.status.Roles, c.status.Traits)
if err != nil {
return nil, trace.Wrap(err)
}

db, err := c.GetDatabase(ctx, dbURI)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
2 changes: 1 addition & 1 deletion lib/teleterm/clusters/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Config struct {
// InsecureSkipVerify is an option to skip TLS cert check
InsecureSkipVerify bool
// Log is a component logger
Log logrus.FieldLogger
Log *logrus.Entry
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with using FieldLogger here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that was because initially I was passing log to the new function (see #13790 (comment)). Then I removed it in favor of using just the default logger but left this change here. Without it the type system didn't let me pass Log as logrus.Entry and from what I remember this is the type that we typically use when passing a logger around.

}

// CheckAndSetDefaults checks the configuration for its validity and sets default values if needed
Expand Down
32 changes: 2 additions & 30 deletions tool/tsh/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package main

import (
"context"
"encoding/base64"
"fmt"
"net"
Expand All @@ -28,7 +27,6 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
dbprofile "github.com/gravitational/teleport/lib/client/db"
"github.com/gravitational/teleport/lib/client/db/dbcmd"
Expand Down Expand Up @@ -80,7 +78,7 @@ func onListDatabases(cf *CLIConf) error {
return trace.Wrap(err)
}

roleSet, err := fetchRoleSet(cf.Context, cluster, profile)
roleSet, err := services.FetchAllClusterRoles(cf.Context, cluster, profile.Roles, profile.Traits)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rosstimothy do you foresee any performance issues with this approach?

if err != nil {
log.Debugf("Failed to fetch user roles: %v.", err)
}
Expand Down Expand Up @@ -143,7 +141,7 @@ func listDatabasesAllClusters(cf *CLIConf) error {
continue
}

roleSet, err := fetchRoleSet(cf.Context, cluster, profile)
roleSet, err := services.FetchAllClusterRoles(cf.Context, cluster, profile.Roles, profile.Traits)
if err != nil {
log.Debugf("Failed to fetch user roles: %v.", err)
}
Expand Down Expand Up @@ -815,32 +813,6 @@ func isMFADatabaseAccessRequired(cf *CLIConf, tc *client.TeleportClient, databas
return mfaResp.GetRequired(), nil
}

// fetchRoleSet fetches a user's roles for a specified cluster.
func fetchRoleSet(ctx context.Context, cluster auth.ClientI, profile *client.ProfileStatus) (services.RoleSet, error) {
// get roles and traits. default to the set from profile, try to get up-to-date version from server point of view.
roles := profile.Roles
traits := profile.Traits

// GetCurrentUser() may not be implemented, fail gracefully.
user, err := cluster.GetCurrentUser(ctx)
if err == nil {
roles = user.GetRoles()
traits = user.GetTraits()
} else {
log.Debugf("Failed to fetch current user information: %v.", err)
}

// get the role definition for all roles of user.
// this may only fail if the role which we are looking for does not exist, or we don't have access to it.
// example scenario when this may happen:
// 1. we have set of roles [foo bar] from profile.
// 2. the cluster is remote and maps the [foo, bar] roles to single role [guest]
// 3. the remote cluster doesn't implement GetCurrentUser(), so we have no way to learn of [guest].
// 4. services.FetchRoles([foo bar], ..., ...) fails as [foo bar] does not exist on remote cluster.
roleSet, err := services.FetchRoles(roles, cluster, traits)
return roleSet, trace.Wrap(err)
}

// pickActiveDatabase returns the database the current profile is logged into.
//
// If logged into multiple databases, returns an error unless one specified
Expand Down