Skip to content

Commit

Permalink
Add tctl decision command (#51094)
Browse files Browse the repository at this point in the history
Implements the new subcommand as described in RFD024e. The only
deviation is in naming, i.e. `tctl decision` instead of the
proposed `decision-service`. All commands are hidden now and
subject to change as development of the PDP is underway.
  • Loading branch information
rosstimothy committed Feb 5, 2025
1 parent f47e886 commit 4e994ae
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 0 deletions.
2 changes: 2 additions & 0 deletions tool/tctl/common/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package common

import (
"github.com/gravitational/teleport/tool/tctl/common/decision"
"github.com/gravitational/teleport/tool/tctl/common/loginrule"
"github.com/gravitational/teleport/tool/tctl/common/plugin"
"github.com/gravitational/teleport/tool/tctl/common/top"
Expand Down Expand Up @@ -57,6 +58,7 @@ func Commands() []CLICommand {
&loginrule.Command{},
&IdPCommand{},
&plugin.PluginsCommand{},
&decision.Command{},
}
}

Expand Down
82 changes: 82 additions & 0 deletions tool/tctl/common/decision/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Teleport
// Copyright (C) 2025 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 decision

import (
"context"
"io"
"os"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"

decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/service/servicecfg"
)

// Command is a group of commands to interact with the Teleport Decision Service.
type Command struct {
// Output is the writer that any command output should be written to.
Output io.Writer

evaluateSSHCommand EvaluateSSHCommand
evaluateDatabaseCommand EvaluateDatabaseCommand
}

// Initialize sets up the "tctl decision" command.
func (c *Command) Initialize(app *kingpin.Application, _ *servicecfg.Config) {
if c.Output == nil {
c.Output = os.Stdout
}

cmd := app.Command("decision", "Interact with the Teleport Decision Service.").Hidden()
c.evaluateSSHCommand.Initialize(cmd, c.Output)
c.evaluateDatabaseCommand.Initialize(cmd, c.Output)
}

// TryRun attempts to run subcommands.
func (c *Command) TryRun(ctx context.Context, cmd string, client *authclient.Client) (bool, error) {
var run func(context.Context, decisionpb.DecisionServiceClient) error
switch cmd {
case c.evaluateSSHCommand.FullCommand():
run = c.evaluateSSHCommand.Run
case c.evaluateDatabaseCommand.FullCommand():
run = c.evaluateDatabaseCommand.Run
default:
return false, nil
}

return true, trace.Wrap(run(ctx, client.DecisionClient()))
}

// WriteProtoJSON outputs the the given [proto.Message] in
// JSON format to the given [io.Writer].
func WriteProtoJSON(w io.Writer, v proto.Message) error {
out, err := protojson.MarshalOptions{
UseProtoNames: true,
Indent: " ",
}.Marshal(v)
if err != nil {
return trace.Wrap(err)
}

_, err = w.Write(out)
return trace.Wrap(err)
}
42 changes: 42 additions & 0 deletions tool/tctl/common/decision/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Teleport
// Copyright (C) 2025 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 decision_test

import (
"context"

"google.golang.org/grpc"

decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
)

type fakeDecisionServiceClient struct {
decisionpb.DecisionServiceClient

sshResponse *decisionpb.EvaluateSSHAccessResponse
databaseResponse *decisionpb.EvaluateDatabaseAccessResponse
}

// EvaluateSSHAccess evaluates an SSH access attempt.
func (f fakeDecisionServiceClient) EvaluateSSHAccess(ctx context.Context, in *decisionpb.EvaluateSSHAccessRequest, opts ...grpc.CallOption) (*decisionpb.EvaluateSSHAccessResponse, error) {
return f.sshResponse, nil
}

// EvaluateDatabaseAccess evaluate a database access attempt.
func (f fakeDecisionServiceClient) EvaluateDatabaseAccess(ctx context.Context, in *decisionpb.EvaluateDatabaseAccessRequest, opts ...grpc.CallOption) (*decisionpb.EvaluateDatabaseAccessResponse, error) {
return f.databaseResponse, nil
}
74 changes: 74 additions & 0 deletions tool/tctl/common/decision/evaluate_db_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Teleport
// Copyright (C) 2025 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 decision

import (
"context"
"io"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"

"github.com/gravitational/teleport"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
"github.com/gravitational/teleport/api/types"
)

// EvaluateDatabaseCommand is a command to evaluate
// database access via the Teleport Decision Service.
type EvaluateDatabaseCommand struct {
output io.Writer
databaseDetails databaseDetails
command *kingpin.CmdClause
}

type databaseDetails struct {
databaseID string
}

// Initialize sets up the "tctl decision evaluate db" command.
func (c *EvaluateDatabaseCommand) Initialize(cmd *kingpin.CmdClause, output io.Writer) {
c.output = output
c.command = cmd.Command("evaluate-db-access", "Evaluate database access for a user.").Hidden()
c.command.Flag("database-id", "The id of the target database.").StringVar(&c.databaseDetails.databaseID)
}

// FullCommand returns the fully qualified name of
// the subcommand, i.e. tctl decision evaluate db.
func (c *EvaluateDatabaseCommand) FullCommand() string {
return c.command.FullCommand()
}

// Run executes the subcommand.
func (c *EvaluateDatabaseCommand) Run(ctx context.Context, clt decisionpb.DecisionServiceClient) error {
resp, err := clt.EvaluateDatabaseAccess(ctx, &decisionpb.EvaluateDatabaseAccessRequest{
Metadata: &decisionpb.RequestMetadata{PepVersionHint: teleport.Version},
TlsIdentity: &decisionpb.TLSIdentity{},
Database: &decisionpb.Resource{
Kind: types.KindDatabase,
Name: c.databaseDetails.databaseID,
},
})
if err != nil {
return trace.Wrap(err)
}

if err := WriteProtoJSON(c.output, resp); err != nil {
return trace.Wrap(err, "failed to marshal result")
}

return nil
}
84 changes: 84 additions & 0 deletions tool/tctl/common/decision/evaluate_db_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Teleport
// Copyright (C) 2025 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 decision_test

import (
"bytes"
"context"
"testing"

"github.com/alecthomas/kingpin/v2"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
"github.com/gravitational/teleport/tool/tctl/common/decision"
)

func TestEvaluateDB(t *testing.T) {
tests := []struct {
name string
response *decisionpb.EvaluateDatabaseAccessResponse
}{
{
name: "denied",
response: &decisionpb.EvaluateDatabaseAccessResponse{
Result: &decisionpb.EvaluateDatabaseAccessResponse_Denial{
Denial: &decisionpb.DatabaseAccessDenial{
Metadata: &decisionpb.DenialMetadata{
PdpVersion: teleport.Version,
UserMessage: "denial",
},
},
},
},
},
{
name: "permitted",
response: &decisionpb.EvaluateDatabaseAccessResponse{
Result: &decisionpb.EvaluateDatabaseAccessResponse_Permit{
Permit: &decisionpb.DatabaseAccessPermit{
Metadata: &decisionpb.PermitMetadata{
PdpVersion: teleport.Version,
},
},
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd := decision.EvaluateDatabaseCommand{}

var output bytes.Buffer
cmd.Initialize(kingpin.New("tctl", "test").Command("decision", ""), &output)

clt := fakeDecisionServiceClient{
databaseResponse: test.response,
}

err := cmd.Run(context.Background(), clt)
require.NoError(t, err, "evaluating database access failed")

var expected bytes.Buffer
err = decision.WriteProtoJSON(&expected, test.response)
require.NoError(t, err, "marshaling expected output failed")
require.Equal(t, output.String(), expected.String(), "output did not match")
})
}
}
79 changes: 79 additions & 0 deletions tool/tctl/common/decision/evaluate_ssh_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Teleport
// Copyright (C) 2025 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 decision

import (
"context"
"io"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"

"github.com/gravitational/teleport"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
"github.com/gravitational/teleport/api/types"
)

// EvaluateSSHCommand is a command to evaluate
// SSH access via the Teleport Decision Service.
type EvaluateSSHCommand struct {
output io.Writer

sshDetails sshDetails
command *kingpin.CmdClause
}

type sshDetails struct {
serverID string
username string
login string
}

// Initialize sets up the "tctl decision evaluate ssh" command.
func (c *EvaluateSSHCommand) Initialize(cmd *kingpin.CmdClause, output io.Writer) {
c.output = output
c.command = cmd.Command("evaluate-ssh-access", "Evaluate SSH access for a user.").Hidden()
c.command.Flag("username", "The username to evaluate access for.").StringVar(&c.sshDetails.username)
c.command.Flag("login", "The os login to evaluate access for.").StringVar(&c.sshDetails.login)
c.command.Flag("server-id", "The host id of the target server.").StringVar(&c.sshDetails.serverID)
}

// FullCommand returns the fully qualified name of
// the subcommand, i.e. tctl decision evaluate ssh.
func (c *EvaluateSSHCommand) FullCommand() string {
return c.command.FullCommand()
}

// Run executes the subcommand.
func (c *EvaluateSSHCommand) Run(ctx context.Context, clt decisionpb.DecisionServiceClient) error {
resp, err := clt.EvaluateSSHAccess(ctx, &decisionpb.EvaluateSSHAccessRequest{
Metadata: &decisionpb.RequestMetadata{PepVersionHint: teleport.Version},
SshIdentity: &decisionpb.SSHIdentity{},
Node: &decisionpb.Resource{
Kind: types.KindNode,
Name: c.sshDetails.serverID,
},
})
if err != nil {
return trace.Wrap(err)
}

if err := WriteProtoJSON(c.output, resp); err != nil {
return trace.Wrap(err, "failed to marshal result")
}

return nil
}
Loading

0 comments on commit 4e994ae

Please sign in to comment.