Skip to content

Commit

Permalink
Add static host user tctl
Browse files Browse the repository at this point in the history
  • Loading branch information
atburke committed Aug 31, 2024
1 parent d507f3c commit 7c04b73
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindAccessGraphSettings, RW()),
types.NewRule(types.KindSPIFFEFederation, RW()),
types.NewRule(types.KindNotification, RW()),
types.NewRule(types.KindStaticHostUser, RW()),
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions lib/services/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ func ParseShortcut(in string) (string, error) {
return types.KindPlugin, nil
case types.KindAccessGraphSettings, "ags":
return types.KindAccessGraphSettings, nil
case types.KindStaticHostUser, types.KindStaticHostUser + "s", "host_user", "host_users":
return types.KindStaticHostUser, nil
}
return "", trace.BadParameter("unsupported resource: %q - resources should be expressed as 'type/name', for example 'connector/github'", in)
}
Expand Down
59 changes: 59 additions & 0 deletions tool/tctl/common/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"io"
"slices"
"sort"
"strconv"
"strings"
Expand All @@ -36,6 +37,7 @@ import (
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
Expand Down Expand Up @@ -1733,3 +1735,60 @@ func (c *spiffeFederationCollection) writeText(w io.Writer, verbose bool) error
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}

type staticHostUserCollection struct {
items []*userprovisioningpb.StaticHostUser
}

func (c *staticHostUserCollection) resources() []types.Resource {
r := make([]types.Resource, 0, len(c.items))
for _, resource := range c.items {
r = append(r, types.Resource153ToLegacy(resource))
}
return r
}

func (c *staticHostUserCollection) writeText(w io.Writer, verbose bool) error {
var rows [][]string
for _, item := range c.items {
groups := make(map[string]struct{})
uids := make(map[string]struct{})
gids := make(map[string]struct{})
labels := make(map[string]struct{})

for _, matcher := range item.Spec.Matchers {
for _, group := range matcher.Groups {
groups[group] = struct{}{}
}
uids[matcher.Uid] = struct{}{}
gids[matcher.Gid] = struct{}{}
for _, label := range matcher.NodeLabels {
labelString := fmt.Sprintf("%s=[%s]", label.Name, printSortedStringSlice(label.Values))
labels[labelString] = struct{}{}
}
}
rows = append(rows, []string{
item.GetMetadata().Name,
strings.Join(utils.StringsSliceFromSet(groups), ","),
strings.Join(utils.StringsSliceFromSet(uids), ","),
strings.Join(utils.StringsSliceFromSet(gids), ","),
strings.Join(utils.StringsSliceFromSet(labels), ","),
})
}
headers := []string{"Login", "Groups", "Uid", "Gid", "Node Labels"}
var t asciitable.Table
if verbose {
t = asciitable.MakeTable(headers, rows...)
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Node Labels")
}
t.SortRowsBy([]int{0}, true)
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}

func printSortedStringSlice(s []string) string {
s = slices.Clone(s)
slices.Sort(s)
return strings.Join(s, ",")
}
56 changes: 56 additions & 0 deletions tool/tctl/common/edit_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ import (
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"

"github.com/gravitational/teleport/api/constants"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
labelv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/label/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/userprovisioning"
"github.com/gravitational/teleport/entitlements"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/backend"
Expand Down Expand Up @@ -73,6 +78,10 @@ func TestEditResources(t *testing.T) {
kind: types.KindSessionRecordingConfig,
edit: testEditSessionRecordingConfig,
},
{
kind: types.KindStaticHostUser,
edit: testEditStaticHostUser,
},
}

for _, test := range tests {
Expand Down Expand Up @@ -485,3 +494,50 @@ func testEditSAMLConnector(t *testing.T, clt *authclient.Client) {
assert.Error(t, err, "stale connector was allowed to be updated")
require.ErrorIs(t, err, backend.ErrIncorrectRevision, "expected an incorrect revision error, got %T", err)
}

func testEditStaticHostUser(t *testing.T, clt *authclient.Client) {
ctx := context.Background()

expected := userprovisioning.NewStaticHostUser("alice", &userprovisioningpb.StaticHostUserSpec{
Matchers: []*userprovisioningpb.Matcher{
{
NodeLabels: []*labelv1.Label{
{
Name: "foo",
Values: []string{"bar"},
},
},
Groups: []string{"foo", "bar"},
},
},
})
created, err := clt.StaticHostUserClient().CreateStaticHostUser(ctx, expected)
require.NoError(t, err)

editor := func(name string) error {
f, err := os.Create(name)
if err != nil {
return trace.Wrap(err, "opening file to edit")
}

expected.GetMetadata().Revision = created.GetMetadata().Revision
expected.Spec.Matchers[0].Groups = []string{"baz", "quux"}

collection := &staticHostUserCollection{items: []*userprovisioningpb.StaticHostUser{expected}}
return trace.NewAggregate(writeYAML(collection, f), f.Close())
}

_, err = runEditCommand(t, clt, []string{"edit", "host_user/alice"}, withEditor(editor))
require.NoError(t, err)

actual, err := clt.StaticHostUserClient().GetStaticHostUser(ctx, expected.GetMetadata().Name)
require.NoError(t, err)
require.Empty(t, cmp.Diff(expected, actual,
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
protocmp.Transform(),
))

_, err = runEditCommand(t, clt, []string{"edit", "host_user/alice"}, withEditor(editor))
require.Error(t, err)
require.True(t, trace.IsCompareFailed(err), "unexpected error: %v", err)
}
66 changes: 66 additions & 0 deletions tool/tctl/common/resource_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
pluginsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/mfa"
Expand Down Expand Up @@ -166,6 +167,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec
types.KindAccessGraphSettings: rc.upsertAccessGraphSettings,
types.KindPlugin: rc.createPlugin,
types.KindSPIFFEFederation: rc.createSPIFFEFederation,
types.KindStaticHostUser: rc.createStaticHostUser,
}
rc.UpdateHandlers = map[ResourceKind]ResourceCreateHandler{
types.KindUser: rc.updateUser,
Expand All @@ -181,6 +183,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec
types.KindVnetConfig: rc.updateVnetConfig,
types.KindAccessGraphSettings: rc.updateAccessGraphSettings,
types.KindPlugin: rc.updatePlugin,
types.KindStaticHostUser: rc.updateStaticHostUser,
}
rc.config = config

Expand Down Expand Up @@ -1419,6 +1422,39 @@ func (rc *ResourceCommand) createServerInfo(ctx context.Context, client *authcli
return nil
}

func (rc *ResourceCommand) createStaticHostUser(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error {
hostUser, err := services.UnmarshalProtoResource[*userprovisioningpb.StaticHostUser](resource.Raw)
if err != nil {
return trace.Wrap(err)
}
c := client.StaticHostUserClient()
if rc.force {
if _, err := c.UpsertStaticHostUser(ctx, hostUser); err != nil {
return trace.Wrap(err)
}
fmt.Printf("static host user %q has been updated\n", hostUser.GetMetadata().Name)
} else {
if _, err := c.CreateStaticHostUser(ctx, hostUser); err != nil {
return trace.Wrap(err)
}
fmt.Printf("static host user %q has been created\n", hostUser.GetMetadata().Name)
}

return nil
}

func (rc *ResourceCommand) updateStaticHostUser(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error {
hostUser, err := services.UnmarshalProtoResource[*userprovisioningpb.StaticHostUser](resource.Raw)
if err != nil {
return trace.Wrap(err)
}
if _, err := client.StaticHostUserClient().UpdateStaticHostUser(ctx, hostUser); err != nil {
return trace.Wrap(err)
}
fmt.Printf("static host user %q has been updated\n", hostUser.GetMetadata().Name)
return nil
}

// Delete deletes resource by name
func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client) (err error) {
singletonResources := []string{
Expand Down Expand Up @@ -1812,6 +1848,11 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client
return trace.Wrap(err)
}
fmt.Printf("SPIFFE federation %q has been deleted\n", rc.ref.Name)
case types.KindStaticHostUser:
if err := client.StaticHostUserClient().DeleteStaticHostUser(ctx, rc.ref.Name); err != nil {
return trace.Wrap(err)
}
fmt.Printf("static host user %q has been deleted\n", rc.ref.Name)
default:
return trace.BadParameter("deleting resources of type %q is not supported", rc.ref.Kind)
}
Expand Down Expand Up @@ -2929,6 +2970,31 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient
}

return &botInstanceCollection{items: instances}, nil
case types.KindStaticHostUser:
hostUserClient := client.StaticHostUserClient()
if rc.ref.Name != "" {
hostUser, err := hostUserClient.GetStaticHostUser(ctx, rc.ref.Name)
if err != nil {
return nil, trace.Wrap(err)
}

return &staticHostUserCollection{items: []*userprovisioningpb.StaticHostUser{hostUser}}, nil
}

var hostUsers []*userprovisioningpb.StaticHostUser
var nextToken string
for {
resp, token, err := hostUserClient.ListStaticHostUsers(ctx, 0, nextToken)
if err != nil {
return nil, trace.Wrap(err)
}
hostUsers = append(hostUsers, resp...)
if token == "" {
break
}
nextToken = token
}
return &staticHostUserCollection{items: hostUsers}, nil
}
return nil, trace.BadParameter("getting %q is not supported", rc.ref.String())
}
Expand Down
71 changes: 71 additions & 0 deletions tool/tctl/common/resource_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/discoveryconfig"
"github.com/gravitational/teleport/api/types/header"
Expand Down Expand Up @@ -1405,6 +1406,10 @@ func TestCreateResources(t *testing.T) {
kind: types.KindAppServer,
create: testCreateAppServer,
},
{
kind: types.KindStaticHostUser,
create: testCreateStaticHostUser,
},
}

for _, test := range tests {
Expand Down Expand Up @@ -2205,6 +2210,72 @@ spec:
require.NoError(t, err)
}

func testCreateStaticHostUser(t *testing.T, clt *authclient.Client) {
// Ensure that our test user does not exist
resourceName := "alice"
resourceKey := types.KindStaticHostUser + "/" + resourceName
_, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
require.Error(t, err)
require.True(t, trace.IsNotFound(err), "unexpected error: %v", err)

const userYAML = `kind: static_host_user
version: v2
metadata:
name: alice
spec:
matchers:
- node_labels:
- name: foo
values: ["bar"]
groups:
- foo
- bar
uid: "1234"
gid: "5678"
- node_labels_expression: 'labels["foo"] == labels["bar"]'
groups:
- baz
- quux
sudoers: ["abc1234"]
`

// Create the host user
userYAMLPath := filepath.Join(t.TempDir(), "host_user.yaml")
require.NoError(t, os.WriteFile(userYAMLPath, []byte(userYAML), 0644))
_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
require.NoError(t, err)

// Fetch the user
buf, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
require.NoError(t, err)
hostUsers := mustDecodeJSON[[]*userprovisioningpb.StaticHostUser](t, buf)
require.Len(t, hostUsers, 1)

var expected userprovisioningpb.StaticHostUser
require.NoError(t, yaml.Unmarshal([]byte(userYAML), &expected))

require.Empty(t, cmp.Diff(
[]*userprovisioningpb.StaticHostUser{&expected},
hostUsers,
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
protocmp.Transform(),
))

// Explicitly change the revision and try creating the user with and without
// the force flag.
expected.GetMetadata().Revision = uuid.NewString()
hostUserBytes, err := services.MarshalProtoResource(&expected, services.PreserveRevision())
require.NoError(t, err)
require.NoError(t, os.WriteFile(userYAMLPath, hostUserBytes, 0644))

_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
require.Error(t, err)
require.True(t, trace.IsAlreadyExists(err), "unexpected error: %v", err)

_, err = runResourceCommand(t, clt, []string{"create", "-f", userYAMLPath})
require.NoError(t, err)
}

func TestPluginResourceWrapper(t *testing.T) {
tests := []struct {
name string
Expand Down

0 comments on commit 7c04b73

Please sign in to comment.