Skip to content
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
5 changes: 3 additions & 2 deletions docs/pages/reference/cli/tsh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ $ tsh request review [<flags>] <request-id>

## tsh request search

Search for resources to request access to
Search for resources to request access to, or list requestable roles.

```code
$ tsh request search [<flags>]
Expand All @@ -1088,7 +1088,8 @@ $ tsh request search [<flags>]

| Name | Default Value(s) | Allowed Value(s) | Description |
| - | - | - | - |
| `--kind` | none | `node`, `kube_cluster`, `kube_resource`, `db`, `app`, `windows_desktop` | Resource kind to search for (required) |
| `--roles` | false | boolean | List requestable roles instead of searching for resources. Mutually exclusive with --kind. |
| `--kind` | none | `node`, `kube_cluster`, `kube_resource`, `db`, `app`, `windows_desktop` | Resource kind to search for. Mutually exclusive with --roles. |
| `--kube-kind` | none | string | Plural name of the kube resource (required when --kind is `kube_resource`) |
| `--kube-api-group` | none | string | API group of the kube resource (required when --kind is `kube_resource` depending on the resource) |
| `--search` | none | Comma-separated strings | List of comma-separated search keywords or phrases enclosed in single quotes |
Expand Down
9 changes: 7 additions & 2 deletions lib/services/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ const (
// `request.kubernetes_resources` config. It's also used to determine if a returned error
// contains this string (in tests and tsh) to customize error messages shown to user.
InvalidKubernetesKindAccessRequest = `your Teleport role's "request.kubernetes_resources" field`

// CannotRequestRole is used in error messages when a user attempts to request
// a role they are not allowed to use. It is also checked in returned errors
// (in tests and tsh) to customize the error message shown to the user.
CannotRequestRole = "can not request role"
)

// ValidateAccessRequest validates the AccessRequest and sets default values
Expand Down Expand Up @@ -1272,11 +1277,11 @@ func (m *RequestValidator) validate(ctx context.Context, req types.AccessRequest
// Roles are normally determined automatically for resource
// access requests, this role must have been explicitly
// requested, or a new deny rule has since been added.
return trace.BadParameter("user %q can not request role %q", req.GetUser(), roleName)
return trace.BadParameter("user %q %s %q", req.GetUser(), CannotRequestRole, roleName)
}
} else {
if !m.CanRequestRole(roleName) {
return trace.BadParameter("user %q can not request role %q", req.GetUser(), roleName)
return trace.BadParameter("user %q %s %q", req.GetUser(), CannotRequestRole, roleName)
}
}
}
Expand Down
66 changes: 66 additions & 0 deletions tool/tsh/common/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package common

import (
"context"
"fmt"
"path"
"sort"
Expand All @@ -34,8 +35,10 @@ import (
"github.com/gravitational/teleport/api/client/proto"
kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/itertools/stream"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
Expand Down Expand Up @@ -406,6 +409,69 @@ func showRequestTable(cf *CLIConf, reqs []types.AccessRequest) error {
}

func onRequestSearch(cf *CLIConf) error {
if cf.RequestableRoles && cf.ResourceKind != "" {
return trace.BadParameter("only one of --kind and --roles may be specified")
}
if !cf.RequestableRoles && cf.ResourceKind == "" {
return trace.BadParameter("one of --kind and --roles is required")
}

if cf.RequestableRoles {
return searchRequestableRoles(cf)
} else {
return searchRequestableResources(cf)
}
}

func searchRequestableRoles(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}

var allRoles []*proto.ListRequestableRolesResponse_RequestableRole
err = tc.WithRootClusterClient(cf.Context, func(clt authclient.ClientI) error {
pageFunc := func(ctx context.Context, pageSize int, pageToken string) ([]*proto.ListRequestableRolesResponse_RequestableRole, string, error) {
req := &proto.ListRequestableRolesRequest{
PageSize: int32(pageSize),
PageToken: pageToken,
}

resp, err := clt.ListRequestableRoles(ctx, req)
return resp.GetRoles(), resp.GetNextPageToken(), trace.Wrap(err)
}

var err error
allRoles, err = stream.Collect(clientutils.Resources(cf.Context, pageFunc))
return err
})
if err != nil {
return trace.Wrap(err)
}

if len(allRoles) == 0 {
fmt.Fprintln(cf.Stdout(), "No requestable roles found.")
Comment thread
tangyatsu marked this conversation as resolved.
return nil
}

columns := []string{"Role", "Description"}
var rows [][]string
for _, r := range allRoles {
rows = append(rows, []string{r.Name, r.Description})
}

var table asciitable.Table
if cf.Verbose {
table = asciitable.MakeTable(columns, rows...)
} else {
table = asciitable.MakeTableWithTruncatedColumn(columns, rows, "Description")
}

_, err = table.AsBuffer().WriteTo(cf.Stdout())
return trace.Wrap(err)
}

func searchRequestableResources(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
Expand Down
121 changes: 121 additions & 0 deletions tool/tsh/common/access_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,124 @@ func TestShowRequestTable(t *testing.T) {
})
}
}

func TestRequestSearchRequestableRoles(t *testing.T) {
ctx := t.Context()
tmpHomePath := t.TempDir()
connector := mockConnector(t)

accessRole, err := types.NewRole("access", types.RoleSpecV6{})
require.NoError(t, err)
accessRole.SetMetadata(types.Metadata{
Name: "access",
Description: "base access role",
})

dbAdminRole, err := types.NewRole("db-admin", types.RoleSpecV6{})
require.NoError(t, err)
dbAdminRole.SetMetadata(types.Metadata{
Name: "db-admin",
Description: "database administrator role",
})

unrequestableRole, err := types.NewRole("unrequestable", types.RoleSpecV6{})
require.NoError(t, err)
unrequestableRole.SetMetadata(types.Metadata{
Name: "unrequestable",
Description: "role that exists but is not requestable for this user",
})

requesterRole, err := types.NewRole("requester", types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"access", "db-admin"},
},
},
})
require.NoError(t, err)

user, err := types.NewUser("alice@example.com")
require.NoError(t, err)
user.SetRoles([]string{"requester"})

auth, proxy := makeTestServers(t,
withBootstrap(
connector,
accessRole,
dbAdminRole,
unrequestableRole,
requesterRole,
user,
),
)
authServer := auth.GetAuthServer()
require.NotNil(t, authServer)

proxyAddr, err := proxy.ProxyWebAddr()
require.NoError(t, err)

err = Run(ctx, []string{
"login",
"--insecure",
"--debug",
"--proxy", proxyAddr.String(),
}, setHomePath(tmpHomePath), setMockSSOLogin(authServer, user, connector.GetName()))
require.NoError(t, err)

wantRolesTable := func() string {
table := asciitable.MakeTable([]string{"Role", "Description"})
table.AddRow([]string{"access", "base access role"})
table.AddRow([]string{"db-admin", "database administrator role"})
return table.AsBuffer().String()
}

tests := []struct {
name string
args []string
// If empty, we expect the command to succeed.
errMessage string
want func() string
}{
{
name: "list requestable roles",
args: []string{"request", "search", "--roles"},
errMessage: "",
want: wantRolesTable,
},
{
name: "both kind and roles set",
args: []string{"request", "search", "--kind=node", "--roles"},
errMessage: "only one of --kind and --roles may be specified",
want: nil,
},
{
name: "neither kind nor roles set",
args: []string{"request", "search"},
errMessage: "one of --kind and --roles is required",
want: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var stdout bytes.Buffer

err := Run(ctx,
append([]string{"--insecure"}, tc.args...),
setHomePath(tmpHomePath),
setCopyStdout(&stdout),
)

if tc.errMessage != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errMessage)
return
}

require.NoError(t, err)
if tc.want != nil {
require.Equal(t, tc.want(), stdout.String())
}
})
}
}
20 changes: 19 additions & 1 deletion tool/tsh/common/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ type CLIConf struct {
AssumeStartTimeRaw string
// ResourceKind is the resource kind to search for
ResourceKind string
// RequestableRoles allows users to search for requestable roles.
RequestableRoles bool
// Username is the Teleport user's username (to login into proxies)
Username string
// ExplicitUsername is true if Username was initially set by the end-user
Expand Down Expand Up @@ -1358,10 +1360,23 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
reqReview.Flag("assume-start-time", "Sets time roles can be assumed by requestor (RFC3339 e.g 2023-12-12T23:20:50.52Z).").StringVar(&cf.AssumeStartTimeRaw)

reqSearch := req.Command("search", "Search for resources to request access to.")
reqSearch.Flag("kind", fmt.Sprintf("Resource kind to search for (%s).", strings.Join(types.RequestableResourceKinds, ", "))).Required().StringVar(&cf.ResourceKind)
reqSearch.Flag("kind", fmt.Sprintf("Resource kind to search for (%s). Mutually exclusive with --roles.", strings.Join(types.RequestableResourceKinds, ", "))).StringVar(&cf.ResourceKind)
reqSearch.Flag("roles", "List requestable roles instead of searching for resources. Mutually exclusive with --kind.").BoolVar(&cf.RequestableRoles)
reqSearch.Flag("kube-kind", fmt.Sprintf("Kubernetes resource kind name (plural) to search for. Required with --kind=%q Ex: pods, deployments, namespaces, etc.", types.KindKubernetesResource)).StringVar(&cf.kubeResourceKind)
reqSearch.Flag("kube-api-group", "Kubernetes API group to search for resources.").StringVar(&cf.kubeAPIGroup)
reqSearch.PreAction(func(*kingpin.ParseContext) error {
if cf.RequestableRoles && cf.ResourceKind != "" {
return trace.BadParameter("only one of --kind and --roles may be specified")
}
if !cf.RequestableRoles && cf.ResourceKind == "" {
return trace.BadParameter("one of --kind and --roles is required")
}

// in --roles mode we don't care about resource kinds or kube flags.
if cf.RequestableRoles {
return nil
}
Comment thread
rosstimothy marked this conversation as resolved.

// TODO(@creack): DELETE IN v20.0.0. Allow legacy kinds with a warning for now.
if slices.Contains(types.LegacyRequestableKubeResourceKinds, cf.ResourceKind) {
cf.kubeAPIGroup = types.KubernetesResourcesV7KindGroups[cf.ResourceKind]
Expand Down Expand Up @@ -3083,6 +3098,9 @@ func executeAccessRequest(cf *CLIConf, tc *client.TeleportClient) error {
if strings.Contains(err.Error(), services.InvalidKubernetesKindAccessRequest) {
return trace.BadParameter("%s\nTry searching for specific kinds with:\n> tsh request search --kube-cluster=KUBE_CLUSTER_NAME --kind=KIND", err.Error())
}
if strings.Contains(err.Error(), services.CannotRequestRole) {
return trace.BadParameter("%s\nHint: run \"tsh request search --roles\" to list requestable roles.", err.Error())
}
return trace.Wrap(err)
}
cf.RequestID = req.GetName()
Expand Down
Loading