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

Add database roles to tsh db ls -v #35582

Merged
merged 6 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 3 additions & 3 deletions tool/tsh/common/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func onListDatabases(cf *CLIConf) error {
}

sort.Sort(types.Databases(databases))
return trace.Wrap(showDatabases(cf.Stdout(), cf.SiteName, databases, activeDatabases, accessChecker, cf.Format, cf.Verbose))
return trace.Wrap(showDatabases(cf, databases, activeDatabases, accessChecker))
}

func accessCheckerForRemoteCluster(ctx context.Context, profile *client.ProfileStatus, proxy *client.ProxyClient, clusterName string) (services.AccessChecker, error) {
Expand Down Expand Up @@ -232,7 +232,7 @@ func listDatabasesAllClusters(cf *CLIConf) error {
format := strings.ToLower(cf.Format)
switch format {
case teleport.Text, "":
printDatabasesWithClusters(cf.SiteName, dbListings, active, cf.Verbose)
printDatabasesWithClusters(cf, dbListings, active)
case teleport.JSON, teleport.YAML:
out, err := serializeDatabasesAllClusters(dbListings, format)
if err != nil {
Expand Down Expand Up @@ -1669,7 +1669,7 @@ func formatAmbiguousDB(cf *CLIConf, selectors resourceSelectors, matchedDBs type
var checker services.AccessChecker
var sb strings.Builder
verbose := true
showDatabasesAsText(&sb, cf.SiteName, matchedDBs, activeDBs, checker, verbose)
showDatabasesAsText(cf, &sb, matchedDBs, activeDBs, checker, verbose)

listCommand := formatDatabaseListCommand(cf.SiteName)
fullNameExample := matchedDBs[0].GetName()
Expand Down
89 changes: 89 additions & 0 deletions tool/tsh/common/db_print.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package common

import (
"fmt"
"io"
"slices"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/services"
)

type printDatabaseTableConfig struct {
writer io.Writer
rows [][]string
showProxyAndCluster bool
verbose bool
excludeColumns []string
}

func (cfg printDatabaseTableConfig) allColumnTitles() []string {
return []string{"Proxy", "Cluster", "Name", "Description", "Protocol", "Type", "URI", "Allowed Users", "Database Roles", "Labels", "Connect"}
}

func printDatabaseTable(cfg printDatabaseTableConfig) {
if !cfg.showProxyAndCluster {
cfg.excludeColumns = append(cfg.excludeColumns, "Proxy", "Cluster")
}
if !cfg.verbose {
cfg.excludeColumns = append(cfg.excludeColumns, "Protocol", "Type", "URI", "Database Roles")
}

var printColumns []string
printRows := make([][]string, len(cfg.rows))

for columnIndex, column := range cfg.allColumnTitles() {
if slices.Contains(cfg.excludeColumns, column) {
continue
}

printColumns = append(printColumns, column)
for rowIndex, row := range cfg.rows {
printRows[rowIndex] = append(printRows[rowIndex], row[columnIndex])
}
}

var t asciitable.Table
if cfg.verbose {
t = asciitable.MakeTable(printColumns, printRows...)
} else {
t = asciitable.MakeTableWithTruncatedColumn(printColumns, printRows, "Labels")
}
fmt.Fprintln(cfg.writer, t.AsBuffer().String())
}

func formatDatabaseRolesForDB(database types.Database, accessChecker services.AccessChecker) string {
if database.SupportsAutoUsers() && database.GetAdminUser().Name != "" {
// may happen if fetching the role set failed for any reason.
if accessChecker == nil {
return "(unknown)"
}

autoUser, roles, err := accessChecker.CheckDatabaseRoles(database)
if err != nil {
log.Warnf("Failed to CheckDatabaseRoles for database %v: %v.", database.GetName(), err)
} else if autoUser.IsEnabled() {
return fmt.Sprintf("%v", roles)
}
}
return ""
}
192 changes: 192 additions & 0 deletions tool/tsh/common/db_print_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package common

import (
"strings"
"testing"

"github.com/stretchr/testify/require"

apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
)

func Test_printDatabaseTable(t *testing.T) {
t.Parallel()

rows := [][]string{
{"proxy", "cluster1", "db1", "describe db1", "postgres", "self-hosted", "localhost:5432", "[*]", "", "Env=dev", "tsh db connect db1"},
{"proxy", "cluster1", "db2", "describe db2", "mysql", "self-hosted", "localhost:3306", "[alice]", "[readonly]", "Env=prod", ""},
}

tests := []struct {
name string
cfg printDatabaseTableConfig
expect string
}{
{
name: "tsh db ls",
cfg: printDatabaseTableConfig{
rows: rows,
showProxyAndCluster: false,
verbose: false,
},
// os.Stdin.Fd() fails during go test, so width is defaulted to 80 for truncated table.
expect: `Name Description Allowed Users Labels Connect
---- ------------ ------------- -------- -------------------
db1 describe db1 [*] Env=dev tsh db connect d...
db2 describe db2 [alice] Env=prod

`,
},
{
name: "tsh db ls -E Description -E Labels -E Connect",
cfg: printDatabaseTableConfig{
rows: rows,
showProxyAndCluster: false,
verbose: false,
excludeColumns: []string{"Description", "Labels", "Connect"},
},
expect: `Name Allowed Users
---- -------------
db1 [*]
db2 [alice]

`,
},
{
name: "tsh db ls -v",
cfg: printDatabaseTableConfig{
rows: rows,
showProxyAndCluster: false,
verbose: true,
},
expect: `Name Description Protocol Type URI Allowed Users Database Roles Labels Connect
---- ------------ -------- ----------- -------------- ------------- -------------- -------- ------------------
db1 describe db1 postgres self-hosted localhost:5432 [*] Env=dev tsh db connect db1
db2 describe db2 mysql self-hosted localhost:3306 [alice] [readonly] Env=prod

`,
},
{
name: "tsh db ls -v -R -E 'Database Roles'",
cfg: printDatabaseTableConfig{
rows: rows,
showProxyAndCluster: true,
verbose: true,
excludeColumns: []string{"Database Roles"},
},
expect: `Proxy Cluster Name Description Protocol Type URI Allowed Users Labels Connect
----- -------- ---- ------------ -------- ----------- -------------- ------------- -------- ------------------
proxy cluster1 db1 describe db1 postgres self-hosted localhost:5432 [*] Env=dev tsh db connect db1
proxy cluster1 db2 describe db2 mysql self-hosted localhost:3306 [alice] Env=prod

`,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var sb strings.Builder

cfg := test.cfg
cfg.writer = &sb

printDatabaseTable(cfg)
require.Equal(t, test.expect, sb.String())
})
}
}

func Test_formatDatabaseRolesForDB(t *testing.T) {
t.Parallel()

db, err := types.NewDatabaseV3(types.Metadata{
Name: "db",
}, types.DatabaseSpecV3{
Protocol: "postgres",
URI: "localhost:5432",
})
require.NoError(t, err)

dbWithAutoUser, err := types.NewDatabaseV3(types.Metadata{
Name: "dbWithAutoUser",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "postgres",
URI: "localhost:5432",
AdminUser: &types.DatabaseAdminUser{
Name: "teleport-admin",
},
})
require.NoError(t, err)

roleAutoUser := &types.RoleV6{
Metadata: types.Metadata{Name: "auto-user", Namespace: apidefaults.Namespace},
Spec: types.RoleSpecV6{
Options: types.RoleOptions{
CreateDatabaseUserMode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
},
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
DatabaseLabels: types.Labels{"env": []string{"prod"}},
DatabaseRoles: []string{"roleA", "roleB"},
DatabaseNames: []string{"*"},
DatabaseUsers: []string{types.Wildcard},
},
},
}

tests := []struct {
name string
database types.Database
accessChecker services.AccessChecker
expect string
}{
{
name: "nil accessChecker",
database: dbWithAutoUser,
expect: "(unknown)",
},
{
name: "roles",
database: dbWithAutoUser,
accessChecker: services.NewAccessCheckerWithRoleSet(&services.AccessInfo{
Username: "alice",
}, "clustername", services.RoleSet{roleAutoUser}),
expect: "[roleA roleB]",
},
{
name: "db without admin user",
database: db,
accessChecker: services.NewAccessCheckerWithRoleSet(&services.AccessInfo{
Username: "alice",
}, "clustername", services.RoleSet{roleAutoUser}),
expect: "",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.expect, formatDatabaseRolesForDB(test.database, test.accessChecker))
})
}
}
Loading
Loading