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
25 changes: 25 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import (
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
secreportsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
summarizerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/summarizer/v1"
Expand Down Expand Up @@ -134,6 +135,7 @@ type AuthServiceClient struct {
userpreferencespb.UserPreferencesServiceClient
notificationsv1pb.NotificationServiceClient
recordingencryptionv1pb.RecordingEncryptionServiceClient
joiningv1.ScopedJoiningServiceClient
}

// Client is a gRPC Client that connects to a Teleport Auth server either
Expand Down Expand Up @@ -547,6 +549,7 @@ func (c *Client) dialGRPC(ctx context.Context, addr string) error {
UserPreferencesServiceClient: userpreferencespb.NewUserPreferencesServiceClient(c.conn),
NotificationServiceClient: notificationsv1pb.NewNotificationServiceClient(c.conn),
RecordingEncryptionServiceClient: recordingencryptionv1pb.NewRecordingEncryptionServiceClient(c.conn),
ScopedJoiningServiceClient: joiningv1.NewScopedJoiningServiceClient(c.conn),
}
c.JoinServiceClient = NewJoinServiceClient(proto.NewJoinServiceClient(c.conn))

Expand Down Expand Up @@ -5811,3 +5814,25 @@ func (c *Client) ValidateTrustedCluster(
}
return resp, nil
}

// ListScopedTokens fetches pages of scoped tokens.
func (c *Client) ListScopedTokens(ctx context.Context, req *joiningv1.ListScopedTokensRequest) (*joiningv1.ListScopedTokensResponse, error) {
res, err := c.grpc.ListScopedTokens(ctx, req)
return res, trace.Wrap(err)
}

// DeleteScopedToken deletes an existing scoped token.
func (c *Client) DeleteScopedToken(ctx context.Context, name string) error {
_, err := c.grpc.DeleteScopedToken(ctx, &joiningv1.DeleteScopedTokenRequest{
Name: name,
})
return trace.Wrap(err)
}

// CreateScopedToken creates a new scoped token.
func (c *Client) CreateScopedToken(ctx context.Context, token *joiningv1.ScopedToken) (*joiningv1.ScopedToken, error) {
res, err := c.grpc.CreateScopedToken(ctx, &joiningv1.CreateScopedTokenRequest{
Token: token,
})
return res.GetToken(), trace.Wrap(err)
}
4 changes: 4 additions & 0 deletions lib/auth/scopes/joining/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ func (s *Server) CreateScopedToken(ctx context.Context, req *scopedjoiningv1.Cre
token.Metadata.Name = name
}

if token.GetSpec() != nil && token.GetSpec().GetJoinMethod() == "" {
token.Spec.JoinMethod = string(types.JoinMethodToken)
Comment thread
nklaassen marked this conversation as resolved.
}

res, err := s.backend.CreateScopedToken(ctx, req)
return res, trace.Wrap(err)
}
Expand Down
108 changes: 108 additions & 0 deletions lib/join/token/scoped.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 token

import (
"strings"
"time"

"github.com/gravitational/trace"

joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
"github.com/gravitational/teleport/api/types"
)

// Scoped wraps a [joiningv1.ScopedToken] such that it can be used to provision
// resources.
type Scoped struct {
token *joiningv1.ScopedToken
joinMethod types.JoinMethod
roles types.SystemRoles
}

// NewScoped returns the wrapped version of the given [joiningv1.ScopedToken].
// It will return an error if the configured join method is not a valid
// [types.JoinMethod] or if any of the configured roles are not a valid
// [types.SystemRole]. The validated join method and roles are cached on the
// [Scoped] wrapper itself so they can be read without repeating validation.
func NewScoped(token *joiningv1.ScopedToken) (*Scoped, error) {
joinMethod := types.JoinMethod(token.GetSpec().GetJoinMethod())
if err := types.ValidateJoinMethod(joinMethod); err != nil {
return nil, trace.Wrap(err)
}

roles, err := types.NewTeleportRoles(token.GetSpec().GetRoles())
if err != nil {
return nil, trace.Wrap(err)
}

return &Scoped{token: token, joinMethod: joinMethod, roles: roles}, nil
}

// GetName returns the name of a [joiningv1.ScopedToken].
func (s *Scoped) GetName() string {
return s.token.GetMetadata().GetName()
}

// GetJoinMethod returns the cached [types.JoinMethod] generated when the
// [joiningv1.ScopedToken] was wrapped.
func (s *Scoped) GetJoinMethod() types.JoinMethod {
return s.joinMethod
}

// GetRoles returns the cached [types.SystemRoles] generated when the
// [joiningv1.ScopedToken] was wrapped.
func (s *Scoped) GetRoles() types.SystemRoles {
return s.roles
}

// GetSafeName returns the name of the scoped token, sanitized appropriately
// for join methods where the name is secret. This should be used when logging
// the token name.
func (s *Scoped) GetSafeName() string {
Comment thread
rosstimothy marked this conversation as resolved.
return GetSafeScopedTokenName(s.token)
}

// Expiry returns the [time.Time] representing when the wrapped
// [joiningv1.ScopedToken] will expire.
func (s *Scoped) Expiry() time.Time {
return s.token.GetMetadata().GetExpires().AsTime()
}

// GetSafeScopedTokenName returns the name of the scoped token, sanitized
// appropriately for join methods where the name is secret. This should be used
// when logging the token name.
func GetSafeScopedTokenName(token *joiningv1.ScopedToken) string {
name := token.GetMetadata().GetName()
if types.JoinMethod(token.GetSpec().GetJoinMethod()) != types.JoinMethodToken {
return name
}

// If the token name is short, we just blank the whole thing.
if len(name) < 16 {
return strings.Repeat("*", len(name))
}

// If the token name is longer, we can show the last 25% of it to help
// the operator identify it.
hiddenBefore := int(0.75 * float64(len(name)))
name = name[hiddenBefore:]
name = strings.Repeat("*", hiddenBefore) + name
return name
}
1 change: 1 addition & 0 deletions tool/tctl/common/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@ func Commands() []CLICommand {
&stableunixusers.Command{},
&decision.Command{},
&BoundKeypairCommand{},
&ScopedCommand{},
}
}
10 changes: 10 additions & 0 deletions tool/tctl/common/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ func runTokensCommand(t *testing.T, client *authclient.Client, args []string) (*
return &stdoutBuff, runCommand(t, client, command, args)
}

func runScopedCommand(t *testing.T, client *authclient.Client, args []string) (*bytes.Buffer, error) {
var stdoutBuff bytes.Buffer
command := &ScopedCommand{
Stdout: &stdoutBuff,
}

args = append([]string{"scoped"}, args...)
return &stdoutBuff, runCommand(t, client, command, args)
}

func runUserCommand(t *testing.T, client *authclient.Client, args []string) error {
command := &UserCommand{}
args = append([]string{"users"}, args...)
Expand Down
60 changes: 60 additions & 0 deletions tool/tctl/common/scoped_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 common

import (
"context"
"io"
"os"

"github.com/alecthomas/kingpin/v2"

"github.com/gravitational/teleport/lib/service/servicecfg"
commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config"
)

// ScopedCommand implements scoped variants of tctl command groups, such as
// `tctl scoped tokens`.
type ScopedCommand struct {
config *servicecfg.Config
tokens *ScopedTokensCommand
Stdout io.Writer
}

// Initialize allows ScopedCommand to plug itself into the CLI parser
func (c *ScopedCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) {
c.config = config
scoped := app.Command("scoped", "Run a subcommand using scoped auth")

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

c.tokens = &ScopedTokensCommand{
Stdout: c.Stdout,
}

c.tokens.Initialize(scoped, config)
}

// TryRun takes the CLI command as an argument (like "scoped tokens") and executes it.
func (c *ScopedCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
return c.tokens.TryRun(ctx, cmd, clientFunc)
}
Loading
Loading