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

Fix tsh db ls for remote clusters. #12281

Merged
merged 14 commits into from
May 23, 2022
Merged
Changes from 12 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
10 changes: 10 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
@@ -607,6 +607,16 @@ func (c *Client) GetUser(name string, withSecrets bool) (types.User, error) {
return user, nil
}

// GetCurrentUser returns current user as seen by the server.
// Useful especially in the context of remote clusters which perform role and trait mapping.
func (c *Client) GetCurrentUser(ctx context.Context) (types.User, error) {
currentUser, err := c.grpc.GetCurrentUser(ctx, &empty.Empty{})
if err != nil {
return nil, trail.FromGRPC(err)
}
return currentUser, nil
}

// GetUsers returns a list of users.
// withSecrets controls whether authentication details are returned.
func (c *Client) GetUsers(withSecrets bool) ([]types.User, error) {
1,256 changes: 649 additions & 607 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions api/client/proto/authservice.proto
Original file line number Diff line number Diff line change
@@ -1775,6 +1775,9 @@ service AuthService {

// GetUser gets a user resource by name.
rpc GetUser(GetUserRequest) returns (types.UserV2);
// GetCurrentUser returns current user as seen by the server.
// Useful especially in the context of remote clusters which perform role and trait mapping.
rpc GetCurrentUser(google.protobuf.Empty) returns (types.UserV2);
// GetUsers gets all current user resources.
rpc GetUsers(GetUsersRequest) returns (stream types.UserV2);
// CreateUser inserts a new user entry to a backend.
19 changes: 19 additions & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import (
"time"

"github.com/coreos/go-semver/semver"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
@@ -1924,6 +1925,24 @@ func (a *ServerWithRoles) GetUser(name string, withSecrets bool) (types.User, er
return a.authServer.Identity.GetUser(name, withSecrets)
}

// GetCurrentUser returns current user as seen by the server.
// Useful especially in the context of remote clusters which perform role and trait mapping.
func (a *ServerWithRoles) GetCurrentUser(ctx context.Context) (types.User, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Without any additional RBAC checks here, I'm a little worried about privilege escalation since we're potentially letting the root's users expand the scope of what they can learn about the leaf cluster. For example, role names - currently they are not available anywhere in the root so users can only see them if they have proper permissions.

I think we should require at least permissions to either read the user's roles or list roles prior to returning this. If the leaf doesn't allow root users view the roles, they're not also gonna be able to see "allowed database users" - which makes sense.

Any other fields are a part of User object we may want to protect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As long as the user has the role, they are allowed to see its definition (from here):

// GetRole returns role by name
func (a *ServerWithRoles) GetRole(ctx context.Context, name string) (types.Role, error) {
	// Current-user exception: we always allow users to read roles
	// that they hold.  This requirement is checked first to avoid
	// misleading denial messages in the logs.
	if !apiutils.SliceContainsStr(a.context.User.GetRoles(), name) {
		if err := a.action(apidefaults.Namespace, types.KindRole, types.VerbRead); err != nil {
			return nil, trace.Wrap(err)
		}
	}
	return a.authServer.GetRole(ctx, name)
}

We don't treat the names of roles as secret. The user would also only get the roles they got from mapping in trusted_cluster resource, which are explicitly given and likely very few.

If we were to do any RBAC checks here, I'm not sure what would they be?

Copy link
Contributor

@smallinsky smallinsky Apr 28, 2022

Choose a reason for hiding this comment

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

The alternative approach would be to move the db user list logic from tsh flow to a backend endpoint. But not sure if we should do it.

Since roles name and trails are public available for a user logged into root cluster not sure what is the reason to hide leaf cluster roles and traits from a user. Thought I think that it would be good idea for now to trim userV2 content and return only to trails and roles name in case of GetCurrentUser preventing the case where userV2 will be extended with some sensitive data.

@Tener
Have you checked if the GetUser can be leveraged here instead of introducing a new GetCurrentUser endpoint. At first glace it looks that authorizeRemoteUser set the correct remote user identity:

user.SetRoles(roleNames)

Groups: user.GetRoles(),

where GetUser already allows to get user's own identity on remote cluster:

func (a *ServerWithRoles) GetUser(name string, withSecrets bool) (types.User, error) {
      ...
		// if secrets are not being accessed, let users always read
		// their own info.
		if err := a.currentUserAction(name); err != nil {
			// not current user, perform normal permission check.
			if err := a.action(apidefaults.Namespace, types.KindUser, types.VerbRead); err != nil {
				return nil, trace.Wrap(err)
			}
		}
	}
	return a.authServer.Identity.GetUser(name, withSecrets)

Copy link
Contributor Author

@Tener Tener Apr 28, 2022

Choose a reason for hiding this comment

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

Have you checked if the GetUser can be leveraged here instead of introducing a new GetCurrentUser endpoint.

Yeah, that was the first thing I tried. GetUser and GetUsers work against real users, provisioned in one way or another.

So a.authServer.Identity.GetUser(name, withSecrets) goes to backend, which is different from a.context.User or a.context.Identity.

Also, the remote users will never satisfy "look at self" check, because we assume no collisions happen:

// The user is prefixed with "remote-" and suffixed with cluster name with
// the hope that it does not match a real local user.
user, err := types.NewUser(fmt.Sprintf("remote-%v-%v", u.Username, u.ClusterName))

Copy link
Collaborator

Choose a reason for hiding this comment

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

@reedloden @russjones Any objections to exposing the user's own leaf cluster identity in this way, provided we add RBAC checks for the role names? It's needed to fetch leaf's role names so tsh can show "allowed database users" for leaf cluster's databases (needed by Teleport Connect also).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@reedloden @russjones Any objections to exposing the user's own leaf cluster identity in this way, provided we add RBAC checks for the role names? It's needed to fetch leaf's role names so tsh can show "allowed database users" for leaf cluster's databases (needed by Teleport Connect also).

@reedloden @russjones can you take a look as per @r0mant request? It would be good to fix this issue as currently the feature is degraded for remote clusters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Tener I talked with @russjones offline about this and there's no objections to exposing user's remote identities to themselves as long as we require read permissions on roles (basically, what I suggested originally). Can you apply the changes? Then we can merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Tener I talked with @russjones offline about this and there's no objections to exposing user's remote identities to themselves as long as we require read permissions on roles (basically, what I suggested originally). Can you apply the changes? Then we can merge.

Thanks for checking, I'll make the change.

// check access to roles
for _, role := range a.context.User.GetRoles() {
_, err := a.GetRole(ctx, role)
if err != nil {
return nil, trace.Wrap(err)
}
}

usrRes := a.context.User.WithoutSecrets()
if usr, ok := usrRes.(types.User); ok {
return usr, nil
}
return nil, trace.BadParameter("expected types.User when fetching current user information, got %T", usrRes)
}

// DeleteUser deletes an existng user in a backend by username.
func (a *ServerWithRoles) DeleteUser(ctx context.Context, user string) error {
if err := a.action(apidefaults.Namespace, types.KindUser, types.VerbDelete); err != nil {
4 changes: 4 additions & 0 deletions lib/auth/clt.go
Original file line number Diff line number Diff line change
@@ -1747,6 +1747,10 @@ type IdentityService interface {
// GetUser returns user by name
GetUser(name string, withSecrets bool) (types.User, error)

// GetCurrentUser returns current user as seen by the server.
// Useful especially in the context of remote clusters which perform role and trait mapping.
GetCurrentUser(ctx context.Context) (types.User, error)

// CreateUser inserts a new entry in a backend.
CreateUser(ctx context.Context, user types.User) error

17 changes: 17 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
@@ -473,6 +473,23 @@ func (g *GRPCServer) GetUser(ctx context.Context, req *proto.GetUserRequest) (*t
return v2, nil
}

func (g *GRPCServer) GetCurrentUser(ctx context.Context, req *empty.Empty) (*types.UserV2, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
user, err := auth.ServerWithRoles.GetCurrentUser(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
v2, ok := user.(*types.UserV2)
if !ok {
log.Warnf("expected type services.UserV2, got %T for user %q", user, user.GetName())
return nil, trace.Errorf("encountered unexpected user type")
}
return v2, nil
}

func (g *GRPCServer) GetUsers(req *proto.GetUsersRequest, stream proto.AuthService_GetUsersServer) error {
auth, err := g.authenticate(stream.Context())
if err != nil {
30 changes: 30 additions & 0 deletions lib/auth/tls_test.go
Original file line number Diff line number Diff line change
@@ -910,6 +910,36 @@ func (s *TLSSuite) TestReadOwnRole(c *check.C) {
fixtures.ExpectAccessDenied(c, err)
}

func TestGetCurrentUser(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)

user1, _, err := CreateUserAndRole(srv.Auth(), "user1", []string{"user1"})
require.NoError(t, err)

client1, err := srv.NewClient(TestIdentity{I: LocalUser{Username: user1.GetName()}})
require.NoError(t, err)

currentUser, err := client1.GetCurrentUser(ctx)
require.NoError(t, err)
require.Equal(t, &types.UserV2{
Kind: "user",
SubKind: "",
Version: "v2",
Metadata: types.Metadata{
Name: "user1",
Namespace: "default",
Description: "",
Labels: nil,
Expires: nil,
ID: 12,
},
Spec: types.UserSpecV2{
Roles: []string{"user:user1"},
},
}, currentUser)
}

func (s *TLSSuite) TestAuthPreference(c *check.C) {
clt, err := s.server.NewClient(TestAdmin())
c.Assert(err, check.IsNil)
22 changes: 21 additions & 1 deletion tool/tsh/db.go
Original file line number Diff line number Diff line change
@@ -73,7 +73,27 @@ func onListDatabases(cf *CLIConf) error {
return trace.Wrap(err)
}

roleSet, err := services.FetchRoles(profile.Roles, cluster, profile.Traits)
// 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(cf.Context)
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)
Copy link
Member

@ravicious ravicious Apr 28, 2022

Choose a reason for hiding this comment

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

Moving the conversation about Teleport Connect so that it doesn't pollute the PR comments.

How do you ensure that for remote clusters c.status.Roles is populated properly?

What I meant in my original comment is that once we get ahold of the auth client in GetAllowedDatabaseUsers before calling FetchRoles, we will be able to use the same logic as tsh uses here, won't we?

We'd call authClient.GetCurrentUser and if that fails we'll just use data from c.status.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, ok, I misunderstood. But yeah, I think that replicating this logic would work just fine.

if err != nil {
log.Debugf("Failed to fetch user roles: %v.", err)
}