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 static host user tctl #46093

Merged
merged 1 commit into from
Sep 4, 2024
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
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 @@ -243,6 +243,8 @@ func ParseShortcut(in string) (string, error) {
return types.KindAccessGraphSettings, nil
case types.KindSPIFFEFederation, types.KindSPIFFEFederation + "s":
return types.KindSPIFFEFederation, 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
61 changes: 61 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,11 +37,13 @@ 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"
"github.com/gravitational/teleport/api/types/discoveryconfig"
"github.com/gravitational/teleport/api/types/externalauditstorage"
"github.com/gravitational/teleport/api/types/label"
"github.com/gravitational/teleport/api/types/secreports"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/asciitable"
Expand Down Expand Up @@ -1733,3 +1736,61 @@ 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 {

for _, matcher := range item.Spec.Matchers {
labelMap := label.ToMap(matcher.NodeLabels)
labelStringMap := make(map[string]string, len(labelMap))
for k, vals := range labelMap {
labelStringMap[k] = fmt.Sprintf("[%s]", printSortedStringSlice(vals))
}
var uid string
if matcher.Uid != 0 {
uid = strconv.Itoa(int(matcher.Uid))
}
var gid string
if matcher.Gid != 0 {
gid = strconv.Itoa(int(matcher.Gid))
}
rows = append(rows, []string{
item.GetMetadata().Name,
common.FormatLabels(labelStringMap, verbose),
matcher.NodeLabelsExpression,
printSortedStringSlice(matcher.Groups),
uid,
gid,
})
}
}
headers := []string{"Login", "Node Labels", "Node Expression", "Groups", "Uid", "Gid"}
var t asciitable.Table
if verbose {
t = asciitable.MakeTable(headers, rows...)
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Node Expression")
GavinFrazar marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading