Skip to content

Commit

Permalink
Make tsh db ls lists available db users. (#10458)
Browse files Browse the repository at this point in the history
* Show available db users in "tsh db ls".

Co-authored-by: Marek Smoliński <[email protected]>
  • Loading branch information
Tener and smallinsky committed Apr 14, 2022
1 parent d095dbe commit 5cba936
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 4 deletions.
104 changes: 104 additions & 0 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,110 @@ func NewRoleSet(roles ...types.Role) RoleSet {
// RoleSet is a set of roles that implements access control functionality
type RoleSet []types.Role

// EnumerationResult is a result of enumerating a role set against some property, e.g. allowed names or logins.
type EnumerationResult struct {
allowedDeniedMap map[string]bool
wildcardAllowed bool
wildcardDenied bool
}

func (result *EnumerationResult) filtered(value bool) []string {
var filtered []string

for entity, allow := range result.allowedDeniedMap {
if allow == value {
filtered = append(filtered, entity)
}
}

sort.Strings(filtered)

return filtered
}

// Denied returns all explicitly denied users.
func (result *EnumerationResult) Denied() []string {
return result.filtered(false)
}

// Allowed returns all known allowed users.
func (result *EnumerationResult) Allowed() []string {
if result.WildcardDenied() {
return nil
}
return result.filtered(true)
}

// WildcardAllowed is true if there * username allowed for given rule set.
func (result *EnumerationResult) WildcardAllowed() bool {
return result.wildcardAllowed && !result.wildcardDenied
}

// WildcardDenied is true if there * username deny for given rule set.
func (result *EnumerationResult) WildcardDenied() bool {
return result.wildcardDenied
}

// NewEnumerationResult returns new EnumerationResult.
func NewEnumerationResult() EnumerationResult {
return EnumerationResult{
allowedDeniedMap: map[string]bool{},
wildcardAllowed: false,
wildcardDenied: false,
}
}

// EnumerateDatabaseUsers works on a given role set to return a minimal description of allowed set of usernames.
// It is biased towards *allowed* usernames; It is meant to describe what the user can do, rather than cannot do.
// For that reason if the user isn't allowed to pick *any* entities, the output will be empty.
//
// In cases where * is listed in set of allowed users, it may be hard for users to figure out the expected username.
// For this reason the parameter extraUsers provides an extra set of users to be checked against RoleSet.
// This extra set of users may be sourced e.g. from user connection history.
func (set RoleSet) EnumerateDatabaseUsers(database types.Database, extraUsers ...string) EnumerationResult {
result := NewEnumerationResult()

// gather users for checking from the roles, check wildcards.
var users []string
for _, role := range set {
wildcardAllowed := false
wildcardDenied := false

for _, user := range role.GetDatabaseUsers(types.Allow) {
if user == types.Wildcard {
wildcardAllowed = true
} else {
users = append(users, user)
}
}

for _, user := range role.GetDatabaseUsers(types.Deny) {
if user == types.Wildcard {
wildcardDenied = true
} else {
users = append(users, user)
}
}

result.wildcardDenied = result.wildcardDenied || wildcardDenied

if err := NewRoleSet(role).CheckAccess(database, AccessMFAParams{Verified: true}); err == nil {
result.wildcardAllowed = result.wildcardAllowed || wildcardAllowed
}

}

users = apiutils.Deduplicate(append(users, extraUsers...))

// check each individual user against the database.
for _, user := range users {
err := set.CheckAccess(database, AccessMFAParams{Verified: true}, &DatabaseUserMatcher{User: user})
result.allowedDeniedMap[user] = err == nil
}

return result
}

// MatchNamespace returns true if given list of namespace matches
// target namespace, wildcard matches everything.
func MatchNamespace(selectors []string, namespace string) (bool, string) {
Expand Down
122 changes: 122 additions & 0 deletions lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2680,6 +2680,128 @@ func TestCheckAccessToDatabaseUser(t *testing.T) {
}
}

func TestRoleSetEnumerateDatabaseUsers(t *testing.T) {
dbStage, err := types.NewDatabaseV3(types.Metadata{
Name: "stage",
Labels: map[string]string{"env": "stage"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
dbProd, err := types.NewDatabaseV3(types.Metadata{
Name: "prod",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
roleDevStage := &types.RoleV5{
Metadata: types.Metadata{Name: "dev-stage", Namespace: apidefaults.Namespace},
Spec: types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseLabels: types.Labels{"env": []string{"stage"}},
DatabaseUsers: []string{types.Wildcard},
},
Deny: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseUsers: []string{"superuser"},
},
},
}
roleDevProd := &types.RoleV5{
Metadata: types.Metadata{Name: "dev-prod", Namespace: apidefaults.Namespace},
Spec: types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseLabels: types.Labels{"env": []string{"prod"}},
DatabaseUsers: []string{"dev"},
},
},
}

roleNoDBAccess := &types.RoleV5{
Metadata: types.Metadata{Name: "no_db_access", Namespace: apidefaults.Namespace},
Spec: types.RoleSpecV5{
Deny: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseUsers: []string{"*"},
DatabaseNames: []string{"*"},
},
},
}

roleAllowDenySame := &types.RoleV5{
Metadata: types.Metadata{Name: "allow_deny_same", Namespace: apidefaults.Namespace},
Spec: types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseUsers: []string{"superuser"},
},
Deny: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseUsers: []string{"superuser"},
},
},
}

testCases := []struct {
name string
roles RoleSet
server types.Database
enumResult EnumerationResult
}{
{
name: "deny overrides allow",
roles: RoleSet{roleAllowDenySame},
server: dbStage,
enumResult: EnumerationResult{
allowedDeniedMap: map[string]bool{"superuser": false},
wildcardAllowed: false,
wildcardDenied: false,
},
},
{
name: "developer allowed any username in stage database except superuser",
roles: RoleSet{roleDevStage, roleDevProd},
server: dbStage,
enumResult: EnumerationResult{
allowedDeniedMap: map[string]bool{"dev": true, "superuser": false},
wildcardAllowed: true,
wildcardDenied: false,
},
},
{
name: "developer allowed only specific username/database in prod database",
roles: RoleSet{roleDevStage, roleDevProd},
server: dbProd,
enumResult: EnumerationResult{
allowedDeniedMap: map[string]bool{"dev": true, "superuser": false},
wildcardAllowed: false,
wildcardDenied: false,
},
},
{
name: "there may be users disallowed from all users",
roles: RoleSet{roleDevStage, roleDevProd, roleNoDBAccess},
server: dbProd,
enumResult: EnumerationResult{
allowedDeniedMap: map[string]bool{"dev": false, "superuser": false},
wildcardAllowed: false,
wildcardDenied: true,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
enumResult := tc.roles.EnumerateDatabaseUsers(tc.server)
require.Equal(t, tc.enumResult, enumResult)
})
}
}

func TestCheckDatabaseNamesAndUsers(t *testing.T) {
roleEmpty := &types.RoleV5{
Metadata: types.Metadata{Name: "roleA", Namespace: apidefaults.Namespace},
Expand Down
22 changes: 21 additions & 1 deletion tool/tsh/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/gravitational/teleport/lib/client"
dbprofile "github.com/gravitational/teleport/lib/client/db"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"

Expand All @@ -49,11 +50,29 @@ func onListDatabases(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}

proxy, err := tc.ConnectToProxy(cf.Context)
if err != nil {
return trace.Wrap(err)
}

cluster, err := proxy.ConnectToCurrentCluster(cf.Context, false)
if err != nil {
return trace.Wrap(err)
}
defer cluster.Close()

// Retrieve profile to be able to show which databases user is logged into.
profile, err := client.StatusCurrent(cf.HomePath, cf.Proxy)
if err != nil {
return trace.Wrap(err)
}

roleSet, err := services.FetchRoles(profile.Roles, cluster, profile.Traits)
if err != nil {
return trace.Wrap(err)
}

sort.Slice(databases, func(i, j int) bool {
return databases[i].GetName() < databases[j].GetName()
})
Expand All @@ -62,7 +81,8 @@ func onListDatabases(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
showDatabases(cf.SiteName, databases, activeDatabases, cf.Verbose)

showDatabases(cf.SiteName, databases, activeDatabases, roleSet, cf.Verbose)
return nil
}

Expand Down
29 changes: 26 additions & 3 deletions tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -1541,9 +1541,29 @@ func showApps(apps []types.Application, active []tlsca.RouteToApp, verbose bool)
}
}

func showDatabases(clusterFlag string, databases []types.Database, active []tlsca.RouteToDatabase, verbose bool) {
func getUsersForDb(database types.Database, roleSet services.RoleSet) string {
dbUsers := roleSet.EnumerateDatabaseUsers(database)
allowed := dbUsers.Allowed()

if dbUsers.WildcardAllowed() {
// start the list with *
allowed = append([]string{types.Wildcard}, allowed...)
}

if len(allowed) == 0 {
return "(none)"
}

denied := dbUsers.Denied()
if len(denied) == 0 || !dbUsers.WildcardAllowed() {
return fmt.Sprintf("%v", allowed)
}
return fmt.Sprintf("%v, except: %v", allowed, denied)
}

func showDatabases(clusterFlag string, databases []types.Database, active []tlsca.RouteToDatabase, roleSet services.RoleSet, verbose bool) {
if verbose {
t := asciitable.MakeTable([]string{"Name", "Description", "Protocol", "Type", "URI", "Labels", "Connect", "Expires"})
t := asciitable.MakeTable([]string{"Name", "Description", "Protocol", "Type", "URI", "Allowed Users", "Labels", "Connect", "Expires"})
for _, database := range databases {
name := database.GetName()
var connect string
Expand All @@ -1553,12 +1573,14 @@ func showDatabases(clusterFlag string, databases []types.Database, active []tlsc
connect = formatConnectCommand(clusterFlag, a)
}
}

t.AddRow([]string{
name,
database.GetDescription(),
database.GetProtocol(),
database.GetType(),
database.GetURI(),
getUsersForDb(database, roleSet),
database.LabelsString(),
connect,
database.Expiry().Format(constants.HumanDateFormatSeconds),
Expand All @@ -1579,11 +1601,12 @@ func showDatabases(clusterFlag string, databases []types.Database, active []tlsc
rows = append(rows, []string{
name,
database.GetDescription(),
getUsersForDb(database, roleSet),
formatDatabaseLabels(database),
connect,
})
}
t := asciitable.MakeTableWithTruncatedColumn([]string{"Name", "Description", "Labels", "Connect"}, rows, "Labels")
t := asciitable.MakeTableWithTruncatedColumn([]string{"Name", "Description", "Allowed Users", "Labels", "Connect"}, rows, "Labels")
fmt.Println(t.AsBuffer().String())
}
}
Expand Down
Loading

0 comments on commit 5cba936

Please sign in to comment.