Skip to content

Commit

Permalink
Add basic tctl commands for WorkloadIdentity resource kind (#49828) (#…
Browse files Browse the repository at this point in the history
…50029)

* Add basic tctl commands for WorkloadIdentity resource kind

* Add `tctl workload-identity ls`

* Add `tctl workload-identity ls`

* Fix message strings to be consistent

* Add tests for `tctl workload-identity` functionality
  • Loading branch information
strideynet authored Dec 11, 2024
1 parent 624e259 commit 7a51453
Show file tree
Hide file tree
Showing 10 changed files with 441 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import (
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
usertaskv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
userpreferencespb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
Expand Down Expand Up @@ -865,6 +866,12 @@ func (c *Client) SPIFFEFederationServiceClient() machineidv1pb.SPIFFEFederationS
return machineidv1pb.NewSPIFFEFederationServiceClient(c.conn)
}

// WorkloadIdentityResourceServiceClient returns an unadorned client for the
// workload identity resource service.
func (c *Client) WorkloadIdentityResourceServiceClient() workloadidentityv1pb.WorkloadIdentityResourceServiceClient {
return workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(c.conn)
}

// PresenceServiceClient returns an unadorned client for the presence service.
func (c *Client) PresenceServiceClient() presencepb.PresenceServiceClient {
return presencepb.NewPresenceServiceClient(c.conn)
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.KindWorkloadIdentity, types.KindWorkloadIdentity + "s", "workload_identities", "workloadidentity", "workloadidentities", "workloadidentitys":
return types.KindWorkloadIdentity, nil
case types.KindStaticHostUser, types.KindStaticHostUser + "s", "host_user", "host_users":
return types.KindStaticHostUser, nil
case types.KindUserTask, types.KindUserTask + "s":
Expand Down
1 change: 1 addition & 0 deletions tool/tctl/common/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func Commands() []CLICommand {
&DesktopCommand{},
&LockCommand{},
&BotsCommand{},
&WorkloadIdentityCommand{},
&InventoryCommand{},
&RecordingsCommand{},
&AlertCommand{},
Expand Down
32 changes: 32 additions & 0 deletions tool/tctl/common/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/api/types/discoveryconfig"
Expand Down Expand Up @@ -1743,6 +1744,37 @@ func (c *spiffeFederationCollection) writeText(w io.Writer, verbose bool) error
return trace.Wrap(err)
}

type workloadIdentityCollection struct {
items []*workloadidentityv1pb.WorkloadIdentity
}

func (c *workloadIdentityCollection) 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 *workloadIdentityCollection) writeText(w io.Writer, verbose bool) error {
headers := []string{"Name", "SPIFFE ID"}

var rows [][]string
for _, item := range c.items {
rows = append(rows, []string{
item.Metadata.Name,
item.GetSpec().GetSpiffe().GetId(),
})
}

t := asciitable.MakeTable(headers, rows...)

// stable sort by name.
t.SortRowsBy([]int{0}, true)
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}

type staticHostUserCollection struct {
items []*userprovisioningpb.StaticHostUser
}
Expand Down
66 changes: 66 additions & 0 deletions tool/tctl/common/resource_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -170,6 +171,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec
types.KindAccessGraphSettings: rc.upsertAccessGraphSettings,
types.KindPlugin: rc.createPlugin,
types.KindSPIFFEFederation: rc.createSPIFFEFederation,
types.KindWorkloadIdentity: rc.createWorkloadIdentity,
types.KindStaticHostUser: rc.createStaticHostUser,
types.KindUserTask: rc.createUserTask,
types.KindAutoUpdateConfig: rc.createAutoUpdateConfig,
Expand Down Expand Up @@ -1035,6 +1037,32 @@ func (rc *ResourceCommand) createSPIFFEFederation(ctx context.Context, client *a
return nil
}

func (rc *ResourceCommand) createWorkloadIdentity(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error {
in, err := services.UnmarshalWorkloadIdentity(raw.Raw)
if err != nil {
return trace.Wrap(err)
}

c := client.WorkloadIdentityResourceServiceClient()
if rc.force {
if _, err := c.UpsertWorkloadIdentity(ctx, &workloadidentityv1pb.UpsertWorkloadIdentityRequest{
WorkloadIdentity: in,
}); err != nil {
return trace.Wrap(err)
}
} else {
if _, err := c.CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{
WorkloadIdentity: in,
}); err != nil {
return trace.Wrap(err)
}
}

fmt.Printf("Workload identity %q has been created\n", in.GetMetadata().GetName())

return nil
}

func (rc *ResourceCommand) updateCrownJewel(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error {
in, err := services.UnmarshalCrownJewel(resource.Raw)
if err != nil {
Expand Down Expand Up @@ -1923,6 +1951,14 @@ 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.KindWorkloadIdentity:
if _, err := client.WorkloadIdentityResourceServiceClient().DeleteWorkloadIdentity(
ctx, &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
Name: rc.ref.Name,
}); err != nil {
return trace.Wrap(err)
}
fmt.Printf("Workload identity %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)
Expand Down Expand Up @@ -3080,6 +3116,36 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient
}

return &spiffeFederationCollection{items: resources}, nil
case types.KindWorkloadIdentity:
if rc.ref.Name != "" {
resource, err := client.WorkloadIdentityResourceServiceClient().GetWorkloadIdentity(ctx, &workloadidentityv1pb.GetWorkloadIdentityRequest{
Name: rc.ref.Name,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &workloadIdentityCollection{items: []*workloadidentityv1pb.WorkloadIdentity{resource}}, nil
}

var resources []*workloadidentityv1pb.WorkloadIdentity
pageToken := ""
for {
resp, err := client.WorkloadIdentityResourceServiceClient().ListWorkloadIdentities(ctx, &workloadidentityv1pb.ListWorkloadIdentitiesRequest{
PageToken: pageToken,
})
if err != nil {
return nil, trace.Wrap(err)
}

resources = append(resources, resp.WorkloadIdentities...)

if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}

return &workloadIdentityCollection{items: resources}, nil
case types.KindStaticHostUser:
hostUserClient := client.StaticHostUserClient()
if rc.ref.Name != "" {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Name SPIFFE ID
---- ---------
test /test

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No workload identities configured
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Workload Identity "test" deleted successfully.
164 changes: 164 additions & 0 deletions tool/tctl/common/workload_identity_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Teleport
// Copyright (C) 2024 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 (
"context"
"fmt"
"io"
"os"

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

"github.com/gravitational/teleport"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
)

// WorkloadIdentityCommand is a group of commands pertaining to Teleport
// Workload Identity.
type WorkloadIdentityCommand struct {
format string
workloadIdentityName string

listCmd *kingpin.CmdClause
rmCmd *kingpin.CmdClause

stdout io.Writer
}

// Initialize sets up the "tctl workload-identity" command.
func (c *WorkloadIdentityCommand) Initialize(
app *kingpin.Application, config *servicecfg.Config,
) {
// TODO(noah): Remove the hidden flag once base functionality is released.
cmd := app.Command(
"workload-identity",
"Manage Teleport Workload Identity.",
).Hidden()

c.listCmd = cmd.Command(
"ls",
"List workload identity configurations.",
)
c.listCmd.
Flag(
"format",
"Output format, 'text' or 'json'",
).
Hidden().
Default(teleport.Text).
EnumVar(&c.format, teleport.Text, teleport.JSON)

c.rmCmd = cmd.Command(
"rm",
"Delete a workload identity configuration.",
)
c.rmCmd.
Arg("name", "Name of the workload identity configuration to delete.").
Required().
StringVar(&c.workloadIdentityName)

if c.stdout == nil {
c.stdout = os.Stdout
}
}

// TryRun attempts to run subcommands.
func (c *WorkloadIdentityCommand) TryRun(
ctx context.Context, cmd string, client *authclient.Client,
) (match bool, err error) {
switch cmd {
case c.listCmd.FullCommand():
err = c.ListWorkloadIdentities(ctx, client)
case c.rmCmd.FullCommand():
err = c.DeleteWorkloadIdentity(ctx, client)
default:
return false, nil
}

return true, trace.Wrap(err)
}

func (c *WorkloadIdentityCommand) DeleteWorkloadIdentity(
ctx context.Context,
client *authclient.Client,
) error {
workloadIdentityClient := client.WorkloadIdentityResourceServiceClient()
_, err := workloadIdentityClient.DeleteWorkloadIdentity(
ctx, &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
Name: c.workloadIdentityName,
})
if err != nil {
return trace.Wrap(err)
}

fmt.Fprintf(
c.stdout,
"Workload Identity %q deleted successfully.\n",
c.workloadIdentityName,
)

return nil
}

// ListWorkloadIdentities writes a listing of the WorkloadIdentity resources
func (c *WorkloadIdentityCommand) ListWorkloadIdentities(
ctx context.Context, client *authclient.Client,
) error {
workloadIdentityClient := client.WorkloadIdentityResourceServiceClient()
var workloadIdentities []*workloadidentityv1pb.WorkloadIdentity
req := &workloadidentityv1pb.ListWorkloadIdentitiesRequest{}
for {
resp, err := workloadIdentityClient.ListWorkloadIdentities(ctx, req)
if err != nil {
return trace.Wrap(err)
}

workloadIdentities = append(
workloadIdentities, resp.WorkloadIdentities...,
)
if resp.NextPageToken == "" {
break
}
req.PageToken = resp.NextPageToken
}

if c.format == teleport.Text {
if len(workloadIdentities) == 0 {
fmt.Fprintln(c.stdout, "No workload identities configured")
return nil
}
t := asciitable.MakeTable([]string{"Name", "SPIFFE ID"})
for _, u := range workloadIdentities {
t.AddRow([]string{
u.GetMetadata().GetName(), u.GetSpec().GetSpiffe().GetId(),
})
}
fmt.Fprintln(c.stdout, t.AsBuffer().String())
} else {
err := utils.WriteJSONArray(c.stdout, workloadIdentities)
if err != nil {
return trace.Wrap(err, "failed to marshal workload identities")
}
}
return nil
}
Loading

0 comments on commit 7a51453

Please sign in to comment.