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
3 changes: 3 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6636,6 +6636,9 @@ message Participant {
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "last_active,omitempty"
];

// Cluster is the cluster name the user is authenticated against.
string Cluster = 5 [(gogoproto.jsontag) = "cluster,omitempty"];
}

// UIConfigV1 represents the configuration for the web UI served by the proxy service
Expand Down
11 changes: 8 additions & 3 deletions api/types/session_tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type SessionTracker interface {
RemoveParticipant(string) error

// UpdatePresence updates presence timestamp of a participant.
UpdatePresence(string, time.Time) error
UpdatePresence(username string, cluster string, t time.Time) error

// GetKubeCluster returns the name of the kubernetes cluster the session is running in.
GetKubeCluster() string
Expand Down Expand Up @@ -320,9 +320,14 @@ func (s *SessionTrackerV1) GetHostUser() string {
}

// UpdatePresence updates presence timestamp of a participant.
func (s *SessionTrackerV1) UpdatePresence(user string, t time.Time) error {
func (s *SessionTrackerV1) UpdatePresence(user, userCluster string, t time.Time) error {
idx := slices.IndexFunc(s.Spec.Participants, func(participant Participant) bool {
return participant.User == user
// participant.Cluster == "" is a legacy participant that was created
// before cluster field was added, so we allow updating presence for
// such participants as well.
// TODO(tigrato): Remove this in version 20.0.0
// TODO(tigrato): DELETE IN 20.0.0
return participant.User == user && (participant.Cluster == userCluster || participant.Cluster == "")
})

if idx < 0 {
Expand Down
14 changes: 12 additions & 2 deletions api/types/session_tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ func TestSessionTrackerV1_UpdatePresence(t *testing.T) {
{
ID: "1",
User: "llama",
Cluster: "teleport-local",
Mode: string(SessionPeerMode),
LastActive: now,
},
{
ID: "2",
User: "fish",
Cluster: "teleport-remote",
Mode: string(SessionModeratorMode),
LastActive: now,
},
{
ID: "3",
User: "cat",
Mode: string(SessionModeratorMode),
LastActive: now,
},
Expand All @@ -48,11 +56,13 @@ func TestSessionTrackerV1_UpdatePresence(t *testing.T) {
require.NoError(t, err)

// Presence cannot be updated for a non-existent user
err = s.UpdatePresence("alpaca", now.Add(time.Hour))
err = s.UpdatePresence("alpaca", "", now.Add(time.Hour))
require.ErrorIs(t, err, trace.NotFound("participant alpaca not found"))

// Update presence for just the user fish
require.NoError(t, s.UpdatePresence("fish", now.Add(time.Hour)))
require.NoError(t, s.UpdatePresence("fish", "teleport-remote", now.Add(time.Hour)))
// Try to Update presence for user fish again, but with a different cluster.
require.Error(t, s.UpdatePresence("fish", "teleport-local", now.Add(time.Hour)))

// Verify that llama has not been active but that fish was
for _, participant := range s.GetParticipants() {
Expand Down
1,379 changes: 712 additions & 667 deletions api/types/types.pb.go

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,10 @@ func (a *ServerWithRoles) filterSessionTracker(joinerRoles []types.Role, tracker
}

for _, participant := range tracker.GetParticipants() {
// We only need to fill in User here since other fields get discarded anyway.
// We only need to fill in User and cluster here since other fields get discarded anyway.
ruleCtx.SSHSession.Parties = append(ruleCtx.SSHSession.Parties, session.Party{
User: participant.User,
User: participant.User,
Cluster: participant.Cluster,
})
}

Expand Down
2 changes: 1 addition & 1 deletion lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ func (c *Client) DeleteClusterAuditConfig(ctx context.Context) error {
return trace.NotImplemented(notImplementedMessage)
}

func (c *Client) UpdatePresence(ctx context.Context, sessionID, user string) error {
func (c *Client) UpdatePresence(ctx context.Context, _, _, _ string) error {
return trace.NotImplemented(notImplementedMessage)
}

Expand Down
26 changes: 21 additions & 5 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2480,12 +2480,28 @@ func doMFAPresenceChallenge(ctx context.Context, actx *grpcContext, stream authp
return trace.Wrap(err)
}

err = actx.authServer.UpdatePresence(ctx, challengeReq.SessionID, user)
if err != nil {
return trace.Wrap(err)
// We must use the real username associated with the identity.
// GetUser() can be remote-{user}-{cluster} if we are using
// a leaf cluster.
realUsername := actx.Identity.GetIdentity().Username
originCluster := actx.Identity.GetIdentity().OriginClusterName
origErr := actx.authServer.UpdatePresence(ctx, challengeReq.SessionID, realUsername, originCluster)
switch {
case trace.IsNotFound(origErr):
// If the user was not found, it may be because the session tracker
// was created with the .GetUser() value (remote-{user}-{cluster}).
// Try again with that username as a fallback.
// TODO(tigrato): DELETE IN 20.0.0
if fallbackErr := actx.authServer.UpdatePresence(ctx, challengeReq.SessionID, user, originCluster); fallbackErr != nil {
// Return the original error, as that is more relevant to the client.
return trace.Wrap(origErr)
}
return nil
case origErr != nil:
return trace.Wrap(origErr)
default:
return nil
}

return nil
}

// MaintainSessionPresence establishes a channel used to continuously verify the presence for a session.
Expand Down
2 changes: 1 addition & 1 deletion lib/events/complete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ func (m *mockSessionTrackerService) RemoveSessionTracker(ctx context.Context, se
return trace.NotImplemented("RemoveSessionTracker is not implemented")
}

func (m *mockSessionTrackerService) UpdatePresence(ctx context.Context, sessionID, user string) error {
func (m *mockSessionTrackerService) UpdatePresence(_ context.Context, _, _, _ string) error {
return trace.NotImplemented("UpdatePresence is not implemented")
}

Expand Down
3 changes: 2 additions & 1 deletion lib/kube/proxy/sess.go
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,8 @@ func (s *session) join(p *party, emitJoinEvent bool) error {
s.log.DebugContext(s.forwarder.ctx, "Tracking participant", "participant_id", p.ID)
participant := &types.Participant{
ID: p.ID.String(),
User: p.Ctx.User.GetName(),
User: p.Ctx.Identity.GetIdentity().Username,
Cluster: p.Ctx.Identity.GetIdentity().OriginClusterName,
Mode: string(p.Mode),
LastActive: time.Now().UTC(),
}
Expand Down
4 changes: 2 additions & 2 deletions lib/services/local/sessiontracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (s *sessionTracker) loadSession(ctx context.Context, sessionID string) (typ
}

// UpdatePresence updates the presence status of a user in a session.
func (s *sessionTracker) UpdatePresence(ctx context.Context, sessionID, user string) error {
func (s *sessionTracker) UpdatePresence(ctx context.Context, sessionID, user, userTeleportCluster string) error {
for range updateRetryLimit {
sessionItem, err := s.bk.Get(ctx, backend.NewKey(sessionPrefix, sessionID))
if err != nil {
Expand All @@ -74,7 +74,7 @@ func (s *sessionTracker) UpdatePresence(ctx context.Context, sessionID, user str
return trace.Wrap(err)
}

if err := session.UpdatePresence(user, s.bk.Clock().Now().UTC()); err != nil {
if err := session.UpdatePresence(user, userTeleportCluster, s.bk.Clock().Now().UTC()); err != nil {
return trace.Wrap(err)
}

Expand Down
9 changes: 8 additions & 1 deletion lib/services/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,14 @@ func toCtxTracker(t types.SessionTracker) ctxTracker {
participants := s.GetParticipants()
names := make([]string, len(participants))
for i, participant := range participants {
names[i] = participant.User
// Participant for RBAC must be represented as `remote-{user}-{cluster}`.
// if they belong to a different cluster. This is because the user
// is also named like that when they authenticate.
names[i] = UsernameForCluster(UsernameForClusterConfig{
User: participant.User,
OriginClusterName: participant.Cluster,
LocalClusterName: s.GetClusterName(),
})
}

return names
Expand Down
96 changes: 96 additions & 0 deletions lib/services/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/vulcand/predicate"
Expand Down Expand Up @@ -824,3 +825,98 @@ func TestCanView(t *testing.T) {
})
}
}

func TestSessionTrackerRBAC(t *testing.T) {
t.Parallel()
localCluster := "local-cluster"
remoteCluster := "remote-cluster"
tracker, err := types.NewSessionTracker(types.SessionTrackerSpecV1{
SessionID: uuid.NewString(),
Kind: types.KindSSHSession,
Hostname: "hostname",
ClusterName: localCluster,
Login: "root",
Participants: []types.Participant{
{
ID: uuid.New().String(),
User: "eve",
Cluster: localCluster,
Mode: string(types.SessionPeerMode),
},
{
ID: uuid.New().String(),
User: "alice",
Cluster: remoteCluster,
Mode: string(types.SessionObserverMode),
},
{
ID: uuid.New().String(),
User: "bob",
Cluster: localCluster,
Mode: string(types.SessionObserverMode),
},
},
Expires: time.Now().UTC().Add(24 * time.Hour),
})
require.NoError(t, err)

testCases := []struct {
name string
username string
userCluster string
checkAccess require.BoolAssertionFunc
}{
{
name: "same user and same cluster as peer participant",
username: "eve",
userCluster: localCluster,
checkAccess: require.True,
},
{
name: "same user but different cluster as peer participant",
username: "eve",
userCluster: remoteCluster,
checkAccess: require.False,
},
{
name: "different user but same cluster as peer participant",
username: "alice",
userCluster: localCluster,
checkAccess: require.False,
},
{
name: "observer participant, same cluster",
username: "bob",
userCluster: localCluster,
checkAccess: require.True,
},
{
name: "not a participant, same cluster",
username: "mallory",
userCluster: localCluster,
checkAccess: require.False,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
user, err := types.NewUser(UsernameForCluster(UsernameForClusterConfig{
User: tc.username,
LocalClusterName: localCluster,
OriginClusterName: tc.userCluster,
}))
require.NoError(t, err)
ctx := &Context{
User: user,
SessionTracker: tracker,
}
parser, err := NewWhereParser(ctx)
require.NoError(t, err)
expr, err := parser.Parse("contains(session_tracker.participants, user.metadata.name)")
require.NoError(t, err)
pred, ok := expr.(predicate.BoolPredicate)
require.True(t, ok)
tc.checkAccess(t, pred())
})
}
}
2 changes: 1 addition & 1 deletion lib/services/sessiontracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type SessionTrackerService interface {
RemoveSessionTracker(ctx context.Context, sessionID string) error

// UpdatePresence updates the presence status of a user in a session.
UpdatePresence(ctx context.Context, sessionID, user string) error
UpdatePresence(ctx context.Context, sessionID, user, userCluster string) error
}

// UnmarshalSessionTracker unmarshals the Session resource from JSON.
Expand Down
23 changes: 23 additions & 0 deletions lib/services/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,26 @@ func MarshalUser(user types.User, opts ...MarshalOption) ([]byte, error) {
func UsernameForRemoteCluster(localUsername, localClusterName string) string {
return fmt.Sprintf("remote-%v-%v", localUsername, localClusterName)
}

// UsernameForClusterConfig is a configuration struct for UsernameForCluster.
type UsernameForClusterConfig struct {
// User is the username.
User string
// OriginClusterName is the cluster name where the user is authenticated.
OriginClusterName string
// LocalClusterName is the local cluster name.
LocalClusterName string
}

// UsernameForCluster returns an username that is prefixed with "remote-"
// and suffixed with cluster name if the user is from a remote cluster,
// otherwise returns the local username.
func UsernameForCluster(cfg UsernameForClusterConfig) string {
// originClusterName == "" is a special case for backward compatibility
// with older clients that do not send origin cluster name.
// In this case we assume the user is local.
if cfg.OriginClusterName == cfg.LocalClusterName || cfg.OriginClusterName == "" {
return cfg.User
}
return UsernameForRemoteCluster(cfg.User, cfg.OriginClusterName)
}
50 changes: 50 additions & 0 deletions lib/services/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,53 @@ func claimsToAttributes(claims map[string]any) saml2.AssertionInfo {
}
return info
}

func TestUsernameForCluster(t *testing.T) {
t.Parallel()

tests := []struct {
username string
originCluster string
localClusterName string
expected string
}{

{
username: "alice",
originCluster: "leaf",
localClusterName: "root",
expected: "remote-alice-leaf",
},
{
username: "bob",
originCluster: "",
localClusterName: "root",
expected: "bob",
},
{
username: "carol",
originCluster: "leaf.cluster",
localClusterName: "root.cluster",
expected: "remote-carol-leaf.cluster",
},
{
username: "dave",
originCluster: "leaf-cluster",
localClusterName: "leaf-cluster",
expected: "dave",
},
}

for _, test := range tests {
t.Run(test.username, func(t *testing.T) {
result := UsernameForCluster(
UsernameForClusterConfig{
User: test.username,
OriginClusterName: test.originCluster,
LocalClusterName: test.localClusterName,
},
)
require.Equal(t, test.expected, result)
})
}
}
2 changes: 2 additions & 0 deletions lib/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ type Party struct {
RemoteAddr string `json:"remote_addr"`
// User is a teleport user using this session
User string `json:"user"`
// Cluster is the cluster name the user is authenticated against.
Cluster string `json:"cluster"`
// ServerID is an address of the server
ServerID string `json:"server_id"`
// LastActive is a last time this party was active
Expand Down
3 changes: 2 additions & 1 deletion lib/srv/app/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ func (c *ConnectionsHandler) createTracker(sess *sessionChunk, identity *tlsca.I
ClusterName: identity.RouteToApp.ClusterName,
Login: identity.GetUserMetadata().Login,
Participants: []types.Participant{{
User: identity.Username,
User: identity.Username,
Cluster: identity.OriginClusterName,
}},
HostUser: identity.Username,
Created: c.cfg.Clock.Now(),
Expand Down
Loading
Loading