Skip to content

Commit

Permalink
Made GetNodes identity aware to only return nodes which that user has
Browse files Browse the repository at this point in the history
access to. In debug mode, made RBAC failures more verbose.
  • Loading branch information
russjones committed Aug 20, 2018
1 parent 9dbbefe commit 617b351
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 68 deletions.
3 changes: 3 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ const (
// ComponentWebsocket is websocket server that the web client connects to.
ComponentWebsocket = "websocket"

// ComponentRBAC is role-based access control.
ComponentRBAC = "rbac"

// DebugEnvVar tells tests to use verbose debug output
DebugEnvVar = "DEBUG"

Expand Down
13 changes: 2 additions & 11 deletions integration/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,34 +552,25 @@ func (i *TeleInstance) CreateEx(trustedSecrets []*InstanceSecrets, tconf *servic
}

// StartNode starts a SSH node and connects it to the cluster.
func (i *TeleInstance) StartNode(name string, sshPort int) (*service.TeleportProcess, error) {
func (i *TeleInstance) StartNode(tconf *service.Config) (*service.TeleportProcess, error) {
dataDir, err := ioutil.TempDir("", "cluster-"+i.Secrets.SiteName)
if err != nil {
return nil, trace.Wrap(err)
}

tconf := service.MakeDefaultConfig()
tconf.DataDir = dataDir

authServer := utils.MustParseAddr(net.JoinHostPort(i.Hostname, i.GetPortAuth()))
tconf.AuthServers = append(tconf.AuthServers, *authServer)
tconf.Token = "token"
tconf.HostUUID = name
tconf.Hostname = name
tconf.DataDir = dataDir
tconf.UploadEventsC = i.UploadEventsC
var ttl time.Duration
tconf.CachePolicy = service.CachePolicy{
Enabled: true,
RecentTTL: &ttl,
}

tconf.Auth.Enabled = false

tconf.Proxy.Enabled = false

tconf.SSH.Enabled = true
tconf.SSH.Addr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", sshPort))

// Create a new Teleport process and add it to the list of nodes that
// compose this "cluster".
process, err := service.NewTeleport(tconf)
Expand Down
150 changes: 149 additions & 1 deletion integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http/httptest"
"net/url"
"os"
Expand Down Expand Up @@ -216,8 +217,20 @@ func (s *IntSuite) TestAuditOn(c *check.C) {
t := s.newTeleportWithConfig(makeConfig())
defer t.Stop(true)

// Start a node.
nodeSSHPort := s.getPorts(1)[0]
nodeProcess, err := t.StartNode("node", nodeSSHPort)
nodeConfig := func() *service.Config {
tconf := service.MakeDefaultConfig()

tconf.HostUUID = "node"
tconf.Hostname = "node"

tconf.SSH.Enabled = true
tconf.SSH.Addr.Addr = net.JoinHostPort(t.Hostname, fmt.Sprintf("%v", nodeSSHPort))

return tconf
}
nodeProcess, err := t.StartNode(nodeConfig())
c.Assert(err, check.IsNil)

// get access to a authClient for the cluster
Expand Down Expand Up @@ -2908,6 +2921,141 @@ func (s *IntSuite) TestWindowChange(c *check.C) {
personA.Type("\aexit\r\n\a")
}

// TestList checks that the list of servers returned is identity aware.
func (s *IntSuite) TestList(c *check.C) {
// Create and start a Teleport cluster with auth, proxy, and node.
makeConfig := func() (*check.C, []string, []*InstanceSecrets, *service.Config) {
clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{
SessionRecording: services.RecordOff,
})
c.Assert(err, check.IsNil)

tconf := service.MakeDefaultConfig()
tconf.Hostname = "server-01"
tconf.Auth.Enabled = true
tconf.Auth.ClusterConfig = clusterConfig
tconf.Proxy.Enabled = true
tconf.Proxy.DisableWebService = true
tconf.Proxy.DisableWebInterface = true
tconf.SSH.Enabled = true
tconf.SSH.Labels = map[string]string{
"role": "worker",
}

return c, nil, nil, tconf
}
t := s.newTeleportWithConfig(makeConfig())
defer t.Stop(true)

// Create and start a Teleport node.
nodeSSHPort := s.getPorts(1)[0]
nodeConfig := func() *service.Config {
tconf := service.MakeDefaultConfig()
tconf.Hostname = "server-02"
tconf.SSH.Enabled = true
tconf.SSH.Addr.Addr = net.JoinHostPort(t.Hostname, fmt.Sprintf("%v", nodeSSHPort))
tconf.SSH.Labels = map[string]string{
"role": "database",
}

return tconf
}
_, err := t.StartNode(nodeConfig())
c.Assert(err, check.IsNil)

// Get an auth client to the cluster.
clt := t.GetSiteAPI(Site)
c.Assert(clt, check.NotNil)

// Wait 10 seconds for both nodes to show up to make sure they both have
// registered themselves.
waitForNodes := func(clt auth.ClientI, count int) error {
tickCh := time.Tick(500 * time.Millisecond)
stopCh := time.After(10 * time.Second)
for {
select {
case <-tickCh:
nodesInCluster, err := clt.GetNodes(defaults.Namespace, services.SkipValidation())
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
if got, want := len(nodesInCluster), count; got == want {
return nil
}
case <-stopCh:
return trace.BadParameter("waited 10s, did find %v nodes", count)
}
}
}
err = waitForNodes(clt, 2)
c.Assert(err, check.IsNil)

var tests = []struct {
inRoleName string
inLabels services.Labels
inLogin string
outNodes []string
}{
// 0 - Role has label "role:worker", only server-01 is returned.
{
inRoleName: "worker-only",
inLogin: "foo",
inLabels: services.Labels{"role": []string{"worker"}},
outNodes: []string{"server-01"},
},
// 1 - Role has label "role:database", only server-02 is returned.
{
inRoleName: "database-only",
inLogin: "bar",
inLabels: services.Labels{"role": []string{"database"}},
outNodes: []string{"server-02"},
},
// 2 - Role has wildcard label, all nodes are returned server-01 and server-2.
{
inRoleName: "worker-and-database",
inLogin: "baz",
inLabels: services.Labels{services.Wildcard: []string{services.Wildcard}},
outNodes: []string{"server-01", "server-02"},
},
}

for _, tt := range tests {
// Create role with logins and labels for this test.
role, err := services.NewRole(tt.inRoleName, services.RoleSpecV3{
Allow: services.RoleConditions{
Logins: []string{tt.inLogin},
NodeLabels: tt.inLabels,
},
})
c.Assert(err, check.IsNil)

// Create user, role, and generate credentials.
err = SetupUser(t.Process, tt.inLogin, []services.Role{role})
c.Assert(err, check.IsNil)
initialCreds, err := GenerateUserCreds(t.Process, tt.inLogin)
c.Assert(err, check.IsNil)

// Create a Teleport client.
cfg := ClientConfig{
Login: tt.inLogin,
Port: t.GetPortSSHInt(),
}
userClt, err := t.NewClientWithCreds(cfg, *initialCreds)
c.Assert(err, check.IsNil)

// Get list of nodes and check that the returned nodes match the
// expected nodes.
nodes, err := userClt.ListNodes(context.Background())
c.Assert(err, check.IsNil)
for _, node := range nodes {
ok := utils.SliceContainsStr(tt.outNodes, node.GetHostname())
if !ok {
c.Fatalf("Got nodes: %v, want: %v.", nodes, tt.outNodes)
}
}
}
}

// runCommand is a shortcut for running SSH command, it creates a client
// connected to proxy of the passed in instance, runs the command, and returns
// the result. If multiple attempts are requested, a 250 millisecond delay is
Expand Down
84 changes: 83 additions & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/trace"

"github.com/sirupsen/logrus"
"github.com/tstranex/u2f"
)

Expand Down Expand Up @@ -85,6 +87,19 @@ func (a *AuthWithRoles) hasBuiltinRole(name string) bool {
return true
}

// hasRemoteBuiltinRole checks the type of the role set returned and the name.
// Returns true if role set is remote builtin and the name matches.
func (a *AuthWithRoles) hasRemoteBuiltinRole(name string) bool {
if _, ok := a.checker.(RemoteBuiltinRoleSet); !ok {
return false
}
if !a.checker.HasRole(name) {
return false
}

return true
}

// AuthenticateWebUser authenticates web user, creates and returns web session
// in case if authentication is successfull
func (a *AuthWithRoles) AuthenticateWebUser(req AuthenticateUserRequest) (services.WebSession, error) {
Expand Down Expand Up @@ -317,11 +332,78 @@ func (a *AuthWithRoles) UpsertNode(s services.Server) error {
return a.authServer.UpsertNode(s)
}

// filterNodes filters nodes based off the role of the logged in user.
func (a *AuthWithRoles) filterNodes(nodes []services.Server) ([]services.Server, error) {
// For certain built-in roles, continue to allow access and return the full
// set of nodes to not break existing clusters during migration. This is
// also used by the proxy to cache a list of all nodes for it's smart
// resolution.
if a.hasBuiltinRole(string(teleport.RoleAdmin)) ||
a.hasBuiltinRole(string(teleport.RoleProxy)) ||
a.hasRemoteBuiltinRole(string(teleport.RoleRemoteProxy)) {
return nodes, nil
}

// Fetch services.RoleSet for the identity of the logged in user.
roles, err := services.FetchRoles(a.user.GetRoles(), a.authServer, a.user.GetTraits())
if err != nil {
return nil, trace.Wrap(err)
}

// Extract all unique allowed logins across all roles.
allowedLogins := make(map[string]bool)
for _, role := range roles {
for _, login := range role.GetLogins(services.Allow) {
allowedLogins[login] = true
}
}

// Loop over all nodes and check if the caller has access.
filteredNodes := make([]services.Server, 0, len(nodes))
NextNode:
for _, node := range nodes {
for login, _ := range allowedLogins {
err := roles.CheckAccessToServer(login, node)
if err == nil {
filteredNodes = append(filteredNodes, node)
continue NextNode
}
}
}

return filteredNodes, nil
}

func (a *AuthWithRoles) GetNodes(namespace string, opts ...services.MarshalOption) ([]services.Server, error) {
if err := a.action(namespace, services.KindNode, services.VerbList); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GetNodes(namespace, opts...)

// Fetch full list of nodes in the backend.
startFetch := time.Now()
nodes, err := a.authServer.GetNodes(namespace, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
elapsedFetch := time.Since(startFetch)

// Filter nodes to return the ones for the connected identity.
startFilter := time.Now()
filteredNodes, err := a.filterNodes(nodes)
if err != nil {
return nil, trace.Wrap(err)
}
elapsedFilter := time.Since(startFilter)

log.WithFields(logrus.Fields{
"user": a.user.GetName(),
"elapsed_fetch": elapsedFetch,
"elapsed_filter": elapsedFilter,
}).Debugf(
"GetServers(%v->%v) in %v.",
len(nodes), len(filteredNodes), elapsedFetch+elapsedFilter)

return filteredNodes, nil
}

func (a *AuthWithRoles) UpsertAuthServer(s services.Server) error {
Expand Down
8 changes: 7 additions & 1 deletion lib/auth/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func (a *authorizer) authorizeRemoteBuiltinRole(r RemoteBuiltinRole) (*AuthConte
user.SetRoles([]string{string(teleport.RoleRemoteProxy)})
return &AuthContext{
User: user,
Checker: roles,
Checker: RemoteBuiltinRoleSet{roles},
}, nil
}

Expand Down Expand Up @@ -473,6 +473,12 @@ type BuiltinRoleSet struct {
services.RoleSet
}

// BuiltinRoleSet wraps a services.RoleSet. The type is used to determine if
// the role is a remote builtin or not.
type RemoteBuiltinRoleSet struct {
services.RoleSet
}

// RemoteBuiltinRole is the role of the remote (service connecting via trusted cluster link)
// Teleport service.
type RemoteBuiltinRole struct {
Expand Down
4 changes: 0 additions & 4 deletions lib/services/local/presence.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ func (s *PresenceService) GetNodes(namespace string, opts ...services.MarshalOpt
return nil, trace.BadParameter("missing namespace value")
}

start := time.Now()

// Get all items in the bucket.
bucket := []string{namespacesPrefix, namespace, nodesPrefix}
items, err := s.GetItems(bucket)
Expand All @@ -191,8 +189,6 @@ func (s *PresenceService) GetNodes(namespace string, opts ...services.MarshalOpt
servers[i] = server
}

s.log.Debugf("GetServers(%v) in %v.", len(servers), time.Now().Sub(start))

return servers, nil
}

Expand Down
Loading

0 comments on commit 617b351

Please sign in to comment.