diff --git a/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go index 8247b914b2458..36b48057c9f8b 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go @@ -310,8 +310,6 @@ type LoggedInUser struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // roles is the user roles Roles []string `protobuf:"bytes,2,rep,name=roles,proto3" json:"roles,omitempty"` - // ssh_logins is the user ssh logins - SshLogins []string `protobuf:"bytes,3,rep,name=ssh_logins,json=sshLogins,proto3" json:"ssh_logins,omitempty"` // acl is a user access control list. // It is available only after the cluster details are fetched, as it is not stored on disk. Acl *ACL `protobuf:"bytes,4,opt,name=acl,proto3" json:"acl,omitempty"` @@ -376,13 +374,6 @@ func (x *LoggedInUser) GetRoles() []string { return nil } -func (x *LoggedInUser) GetSshLogins() []string { - if x != nil { - return x.SshLogins - } - return nil -} - func (x *LoggedInUser) GetAcl() *ACL { if x != nil { return x.Acl @@ -778,12 +769,10 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" + " \x01(\tR\fproxyVersion\x12N\n" + "\x0eshow_resources\x18\v \x01(\x0e2'.teleport.lib.teleterm.v1.ShowResourcesR\rshowResources\x120\n" + "\x14profile_status_error\x18\f \x01(\tR\x12profileStatusError\x12\x19\n" + - "\bsso_host\x18\r \x01(\tR\assoHost\"\xb7\x04\n" + + "\bsso_host\x18\r \x01(\tR\assoHost\"\xaa\x04\n" + "\fLoggedInUser\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05roles\x18\x02 \x03(\tR\x05roles\x12\x1d\n" + - "\n" + - "ssh_logins\x18\x03 \x03(\tR\tsshLogins\x12/\n" + + "\x05roles\x18\x02 \x03(\tR\x05roles\x12/\n" + "\x03acl\x18\x04 \x01(\v2\x1d.teleport.lib.teleterm.v1.ACLR\x03acl\x12'\n" + "\x0factive_requests\x18\x05 \x03(\tR\x0eactiveRequests\x12/\n" + "\x13suggested_reviewers\x18\x06 \x03(\tR\x12suggestedReviewers\x12+\n" + @@ -795,7 +784,8 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" + "\bUserType\x12\x19\n" + "\x15USER_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fUSER_TYPE_LOCAL\x10\x01\x12\x11\n" + - "\rUSER_TYPE_SSO\x10\x02\"\xe9\b\n" + + "\rUSER_TYPE_SSO\x10\x02J\x04\b\x03\x10\x04R\n" + + "ssh_logins\"\xe9\b\n" + "\x03ACL\x12Q\n" + "\x0fauth_connectors\x18\x02 \x01(\v2(.teleport.lib.teleterm.v1.ResourceAccessR\x0eauthConnectors\x12>\n" + "\x05roles\x18\x03 \x01(\v2(.teleport.lib.teleterm.v1.ResourceAccessR\x05roles\x12>\n" + diff --git a/gen/proto/go/teleport/lib/teleterm/v1/server.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/server.pb.go index 8d4d87c4d7dca..1d180734c72b8 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/server.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/server.pb.go @@ -54,7 +54,9 @@ type Server struct { // labels is this server list of labels Labels []*Label `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty"` // node sub kind: teleport, openssh, openssh-ec2-ice - SubKind string `protobuf:"bytes,7,opt,name=sub_kind,json=subKind,proto3" json:"sub_kind,omitempty"` + SubKind string `protobuf:"bytes,7,opt,name=sub_kind,json=subKind,proto3" json:"sub_kind,omitempty"` + // Subset of logins allowed by the certificate and RBAC rules. + Logins []string `protobuf:"bytes,8,rep,name=logins,proto3" json:"logins,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -138,11 +140,18 @@ func (x *Server) GetSubKind() string { return "" } +func (x *Server) GetLogins() []string { + if x != nil { + return x.Logins + } + return nil +} + var File_teleport_lib_teleterm_v1_server_proto protoreflect.FileDescriptor const file_teleport_lib_teleterm_v1_server_proto_rawDesc = "" + "\n" + - "%teleport/lib/teleterm/v1/server.proto\x12\x18teleport.lib.teleterm.v1\x1a$teleport/lib/teleterm/v1/label.proto\"\xca\x01\n" + + "%teleport/lib/teleterm/v1/server.proto\x12\x18teleport.lib.teleterm.v1\x1a$teleport/lib/teleterm/v1/label.proto\"\xe2\x01\n" + "\x06Server\x12\x10\n" + "\x03uri\x18\x01 \x01(\tR\x03uri\x12\x16\n" + "\x06tunnel\x18\x02 \x01(\bR\x06tunnel\x12\x12\n" + @@ -150,7 +159,8 @@ const file_teleport_lib_teleterm_v1_server_proto_rawDesc = "" + "\bhostname\x18\x04 \x01(\tR\bhostname\x12\x12\n" + "\x04addr\x18\x05 \x01(\tR\x04addr\x127\n" + "\x06labels\x18\x06 \x03(\v2\x1f.teleport.lib.teleterm.v1.LabelR\x06labels\x12\x19\n" + - "\bsub_kind\x18\a \x01(\tR\asubKindBTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" + "\bsub_kind\x18\a \x01(\tR\asubKind\x12\x16\n" + + "\x06logins\x18\b \x03(\tR\x06loginsBTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" var ( file_teleport_lib_teleterm_v1_server_proto_rawDescOnce sync.Once diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts index 153c807785f4d..a8273314b07bb 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts @@ -146,12 +146,6 @@ export interface LoggedInUser { * @generated from protobuf field: repeated string roles = 2; */ roles: string[]; - /** - * ssh_logins is the user ssh logins - * - * @generated from protobuf field: repeated string ssh_logins = 3; - */ - sshLogins: string[]; /** * acl is a user access control list. * It is available only after the cluster details are fetched, as it is not stored on disk. @@ -539,7 +533,6 @@ class LoggedInUser$Type extends MessageType { super("teleport.lib.teleterm.v1.LoggedInUser", [ { no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "roles", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, - { no: 3, name: "ssh_logins", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, { no: 4, name: "acl", kind: "message", T: () => ACL }, { no: 5, name: "active_requests", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, { no: 6, name: "suggested_reviewers", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, @@ -553,7 +546,6 @@ class LoggedInUser$Type extends MessageType { const message = globalThis.Object.create((this.messagePrototype!)); message.name = ""; message.roles = []; - message.sshLogins = []; message.activeRequests = []; message.suggestedReviewers = []; message.requestableRoles = []; @@ -575,9 +567,6 @@ class LoggedInUser$Type extends MessageType { case /* repeated string roles */ 2: message.roles.push(reader.string()); break; - case /* repeated string ssh_logins */ 3: - message.sshLogins.push(reader.string()); - break; case /* teleport.lib.teleterm.v1.ACL acl */ 4: message.acl = ACL.internalBinaryRead(reader, reader.uint32(), options, message.acl); break; @@ -617,9 +606,6 @@ class LoggedInUser$Type extends MessageType { /* repeated string roles = 2; */ for (let i = 0; i < message.roles.length; i++) writer.tag(2, WireType.LengthDelimited).string(message.roles[i]); - /* repeated string ssh_logins = 3; */ - for (let i = 0; i < message.sshLogins.length; i++) - writer.tag(3, WireType.LengthDelimited).string(message.sshLogins[i]); /* teleport.lib.teleterm.v1.ACL acl = 4; */ if (message.acl) ACL.internalBinaryWrite(message.acl, writer.tag(4, WireType.LengthDelimited).fork(), options).join(); diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/server_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/server_pb.ts index f87dddb1f1acc..3f44b6bd3c288 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/server_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/server_pb.ts @@ -79,6 +79,12 @@ export interface Server { * @generated from protobuf field: string sub_kind = 7; */ subKind: string; + /** + * Subset of logins allowed by the certificate and RBAC rules. + * + * @generated from protobuf field: repeated string logins = 8; + */ + logins: string[]; } // @generated message type with reflection information, may provide speed optimized methods class Server$Type extends MessageType { @@ -90,7 +96,8 @@ class Server$Type extends MessageType { { no: 4, name: "hostname", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 5, name: "addr", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 6, name: "labels", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Label }, - { no: 7, name: "sub_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + { no: 7, name: "sub_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 8, name: "logins", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): Server { @@ -102,6 +109,7 @@ class Server$Type extends MessageType { message.addr = ""; message.labels = []; message.subKind = ""; + message.logins = []; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -132,6 +140,9 @@ class Server$Type extends MessageType { case /* string sub_kind */ 7: message.subKind = reader.string(); break; + case /* repeated string logins */ 8: + message.logins.push(reader.string()); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -165,6 +176,9 @@ class Server$Type extends MessageType { /* string sub_kind = 7; */ if (message.subKind !== "") writer.tag(7, WireType.LengthDelimited).string(message.subKind); + /* repeated string logins = 8; */ + for (let i = 0; i < message.logins.length; i++) + writer.tag(8, WireType.LengthDelimited).string(message.logins[i]); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/lib/client/api.go b/lib/client/api.go index 50f7e4fa5f8fd..b1399ad0b7c96 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -5608,3 +5608,26 @@ func (tc *TeleportClient) DialDatabase(ctx context.Context, route proto.RouteToD return tc.DialALPN(ctx, cert, alpnProtocol) } + +// CalculateSSHLogins returns the subset of the allowedLogins that exist in +// the principals of the identity. This is required because SSH authorization +// only allows using a login that exists in the certificates valid principals. +// When connecting to servers in a leaf cluster, the root certificate is used, +// so we need to ensure that we only present the allowed logins that would +// result in a successful connection, if any exists. +func CalculateSSHLogins(identityPrincipals []string, allowedLogins []string) ([]string, error) { + allowed := make(map[string]struct{}) + for _, login := range allowedLogins { + allowed[login] = struct{}{} + } + + var logins []string + for _, local := range identityPrincipals { + if _, ok := allowed[local]; ok { + logins = append(logins, local) + } + } + + slices.Sort(logins) + return logins, nil +} diff --git a/lib/client/api_test.go b/lib/client/api_test.go index f9ee21bde2687..9984cf601be7a 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -26,11 +26,13 @@ import ( "io" "math" "os" + "strings" "testing" "time" "github.com/coreos/go-semver/semver" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1647,3 +1649,48 @@ func TestParsePortMapping(t *testing.T) { }) } } + +func TestCalculateSSHLogins(t *testing.T) { + cases := []struct { + name string + allowedLogins []string + grantedPrincipals []string + expectedLogins []string + }{ + { + name: "no matching logins", + allowedLogins: []string{"llama"}, + grantedPrincipals: []string{"fish"}, + }, + { + name: "identical logins", + allowedLogins: []string{"llama", "shark", "goose"}, + grantedPrincipals: []string{"shark", "goose", "llama"}, + expectedLogins: []string{"goose", "shark", "llama"}, + }, + { + name: "subset of logins", + allowedLogins: []string{"llama"}, + grantedPrincipals: []string{"shark", "goose", "llama"}, + expectedLogins: []string{"llama"}, + }, + { + name: "no allowed logins", + grantedPrincipals: []string{"shark", "goose", "llama"}, + }, + { + name: "no granted logins", + allowedLogins: []string{"shark", "goose", "llama"}, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + logins, err := CalculateSSHLogins(test.grantedPrincipals, test.allowedLogins) + require.NoError(t, err) + require.Empty(t, cmp.Diff(logins, test.expectedLogins, cmpopts.SortSlices(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }))) + }) + } +} diff --git a/lib/teleterm/apiserver/handler/handler_clusters.go b/lib/teleterm/apiserver/handler/handler_clusters.go index 2dac4b9e511d0..68769b0194473 100644 --- a/lib/teleterm/apiserver/handler/handler_clusters.go +++ b/lib/teleterm/apiserver/handler/handler_clusters.go @@ -101,7 +101,6 @@ func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster { Connected: cluster.Connected(), LoggedInUser: &api.LoggedInUser{ Name: loggedInUser.Name, - SshLogins: loggedInUser.SSHLogins, Roles: loggedInUser.Roles, ActiveRequests: loggedInUser.ActiveRequests, IsDeviceTrusted: cluster.HasDeviceTrustExtensions(), @@ -155,9 +154,8 @@ func newAPILeafCluster(leaf clusters.LeafCluster) *api.Cluster { Connected: leaf.Connected, Leaf: true, LoggedInUser: &api.LoggedInUser{ - Name: leaf.LoggedInUser.Name, - SshLogins: leaf.LoggedInUser.SSHLogins, - Roles: leaf.LoggedInUser.Roles, + Name: leaf.LoggedInUser.Name, + Roles: leaf.LoggedInUser.Roles, }, } } diff --git a/lib/teleterm/apiserver/handler/handler_unified_resources.go b/lib/teleterm/apiserver/handler/handler_unified_resources.go index 11c95e305df88..8073ebfed0f2b 100644 --- a/lib/teleterm/apiserver/handler/handler_unified_resources.go +++ b/lib/teleterm/apiserver/handler/handler_unified_resources.go @@ -132,5 +132,6 @@ func newAPIServer(server clusters.Server) *api.Server { Addr: server.GetAddr(), SubKind: server.GetSubKind(), Labels: apiLabels, + Logins: server.Logins, } } diff --git a/lib/teleterm/clusters/cluster.go b/lib/teleterm/clusters/cluster.go index 0d92e2fe98068..326ad470fe5d2 100644 --- a/lib/teleterm/clusters/cluster.go +++ b/lib/teleterm/clusters/cluster.go @@ -390,8 +390,10 @@ func UserTypeFromString(userType types.UserType) (api.LoggedInUser_UserType, err // Server describes an SSH node. type Server struct { - // URI is the database URI + // URI is the cluster URI URI uri.ResourceURI + // Subset of logins allowed by the certificate and RBAC rules. + Logins []string types.Server } diff --git a/lib/teleterm/services/unifiedresources/unifiedresources.go b/lib/teleterm/services/unifiedresources/unifiedresources.go index 968606e162e01..0dc9545263fd7 100644 --- a/lib/teleterm/services/unifiedresources/unifiedresources.go +++ b/lib/teleterm/services/unifiedresources/unifiedresources.go @@ -27,6 +27,7 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/teleterm/clusters" ) @@ -67,10 +68,15 @@ func List(ctx context.Context, cluster *clusters.Cluster, client apiclient.ListU requiresRequest := enrichedResource.RequiresRequest switch r := enrichedResource.ResourceWithLabels.(type) { case types.Server: + logins, err := libclient.CalculateSSHLogins(cluster.GetLoggedInUser().SSHLogins, enrichedResource.Logins) + if err != nil { + return nil, trace.Wrap(err) + } response.Resources = append(response.Resources, UnifiedResource{ Server: &clusters.Server{ URI: cluster.URI.AppendServer(r.GetName()), Server: r, + Logins: logins, }, RequiresRequest: requiresRequest, }) diff --git a/lib/teleterm/services/unifiedresources/unififedresources_test.go b/lib/teleterm/services/unifiedresources/unififedresources_test.go index 15e74c2ccd80d..052448980d191 100644 --- a/lib/teleterm/services/unifiedresources/unififedresources_test.go +++ b/lib/teleterm/services/unifiedresources/unififedresources_test.go @@ -115,7 +115,7 @@ func TestUnifiedResourcesList(t *testing.T) { require.NoError(t, err) mockedResources := []*proto.PaginatedResource{ - {Resource: &proto.PaginatedResource_Node{Node: node.(*types.ServerV2)}}, + {Resource: &proto.PaginatedResource_Node{Node: node.(*types.ServerV2)}, Logins: []string{"ec2-user"}}, {Resource: &proto.PaginatedResource_DatabaseServer{DatabaseServer: database}}, {Resource: &proto.PaginatedResource_KubernetesServer{KubernetesServer: kube}}, {Resource: &proto.PaginatedResource_AppServer{AppServer: app}}, @@ -136,6 +136,7 @@ func TestUnifiedResourcesList(t *testing.T) { require.Equal(t, UnifiedResource{Server: &clusters.Server{ URI: uri.NewClusterURI(cluster.ProfileName).AppendServer(node.GetName()), Server: node, + Logins: nil, // because the cluster has no SSH logins }}, response.Resources[0]) require.Equal(t, UnifiedResource{Database: &clusters.Database{ diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 8572b7590f916..c4da188e90324 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -103,7 +103,6 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/readonly" "github.com/gravitational/teleport/lib/session" - "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/web/app" @@ -3260,29 +3259,6 @@ type loginGetter interface { GetAllowedLoginsForResource(resource services.AccessCheckable) ([]string, error) } -// calculateSSHLogins returns the subset of the allowedLogins that exist in -// the principals of the identity. This is required because SSH authorization -// only allows using a login that exists in the certificates valid principals. -// When connecting to servers in a leaf cluster, the root certificate is used, -// so we need to ensure that we only present the allowed logins that would -// result in a successful connection, if any exists. -func calculateSSHLogins(identity *tlsca.Identity, allowedLogins []string) ([]string, error) { - allowed := make(map[string]struct{}) - for _, login := range allowedLogins { - allowed[login] = struct{}{} - } - - var logins []string - for _, local := range identity.Principals { - if _, ok := allowed[local]; ok { - logins = append(logins, local) - } - } - - slices.Sort(logins) - return logins, nil -} - // calculateAppLogins determines the app logins allowed for the provided // resource. // @@ -3363,7 +3339,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt case types.Server: switch enriched.GetKind() { case types.KindNode: - logins, err := calculateSSHLogins(identity, enriched.Logins) + logins, err := client.CalculateSSHLogins(identity.Principals, enriched.Logins) if err != nil { return nil, trace.Wrap(err) } @@ -3462,7 +3438,7 @@ func (h *Handler) clusterNodesGet(w http.ResponseWriter, r *http.Request, p http continue } - logins, err := calculateSSHLogins(identity, resource.Logins) + logins, err := client.CalculateSSHLogins(identity.Principals, resource.Logins) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 11a7193aa79cb..10f11054aa3f3 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -10716,53 +10716,6 @@ func TestGithubConnector(t *testing.T) { assert.Equal(t, http.StatusOK, resp.Code(), "unexpected status code getting connectors") } -func TestCalculateSSHLogins(t *testing.T) { - cases := []struct { - name string - allowedLogins []string - grantedPrincipals []string - expectedLogins []string - }{ - { - name: "no matching logins", - allowedLogins: []string{"llama"}, - grantedPrincipals: []string{"fish"}, - }, - { - name: "identical logins", - allowedLogins: []string{"llama", "shark", "goose"}, - grantedPrincipals: []string{"shark", "goose", "llama"}, - expectedLogins: []string{"goose", "shark", "llama"}, - }, - { - name: "subset of logins", - allowedLogins: []string{"llama"}, - grantedPrincipals: []string{"shark", "goose", "llama"}, - expectedLogins: []string{"llama"}, - }, - { - name: "no allowed logins", - grantedPrincipals: []string{"shark", "goose", "llama"}, - }, - { - name: "no granted logins", - allowedLogins: []string{"shark", "goose", "llama"}, - }, - } - - for _, test := range cases { - t.Run(test.name, func(t *testing.T) { - identity := &tlsca.Identity{Principals: test.grantedPrincipals} - - logins, err := calculateSSHLogins(identity, test.allowedLogins) - require.NoError(t, err) - require.Empty(t, cmp.Diff(logins, test.expectedLogins, cmpopts.SortSlices(func(a, b string) bool { - return strings.Compare(a, b) < 0 - }))) - }) - } -} - func TestCalculateAppLogins(t *testing.T) { cases := []struct { name string diff --git a/proto/teleport/lib/teleterm/v1/cluster.proto b/proto/teleport/lib/teleterm/v1/cluster.proto index 09a8bfb49f3da..ed95f9c70ed63 100644 --- a/proto/teleport/lib/teleterm/v1/cluster.proto +++ b/proto/teleport/lib/teleterm/v1/cluster.proto @@ -83,8 +83,9 @@ message LoggedInUser { string name = 1; // roles is the user roles repeated string roles = 2; - // ssh_logins is the user ssh logins - repeated string ssh_logins = 3; + + reserved "ssh_logins"; + reserved 3; // acl is a user access control list. // It is available only after the cluster details are fetched, as it is not stored on disk. ACL acl = 4; diff --git a/proto/teleport/lib/teleterm/v1/server.proto b/proto/teleport/lib/teleterm/v1/server.proto index 44ab85761f22e..3f15f37861b53 100644 --- a/proto/teleport/lib/teleterm/v1/server.proto +++ b/proto/teleport/lib/teleterm/v1/server.proto @@ -40,4 +40,6 @@ message Server { repeated Label labels = 6; // node sub kind: teleport, openssh, openssh-ec2-ice string sub_kind = 7; + // Subset of logins allowed by the certificate and RBAC rules. + repeated string logins = 8; } diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index 5d1ee5ff947b2..5de20d959d921 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -42,6 +42,7 @@ export const makeServer = (props: Partial = {}): tsh.Server => ({ addr: '127.0.0.1:3022', labels: [], subKind: 'teleport', + logins: ['ec2-user'], ...props, }); @@ -254,7 +255,6 @@ export const makeLoggedInUser = ( isDeviceTrusted: false, trustedDeviceRequirement: TrustedDeviceRequirement.NOT_REQUIRED, acl: makeAcl(), - sshLogins: [], roles: [], requestableRoles: [], suggestedReviewers: [], diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx index 3cef6fd2fb802..4f7109eb1a15c 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx @@ -150,7 +150,6 @@ function Buttons(props: StoryProps) { } const testCluster = makeRootCluster(); -testCluster.loggedInUser.sshLogins = ['ec2-user']; function prepareAppContext(appContext: MockAppContext): void { appContext.workspacesService.setState(d => { diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx index 8a9a2759ed2d2..1aadfe335a428 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx @@ -78,11 +78,6 @@ export function ConnectServerActionButton(props: { }); } - function getSshLogins(): string[] { - const cluster = ctx.clustersService.findClusterByResource(props.server.uri); - return cluster?.loggedInUser?.sshLogins || []; - } - function connect(login: string): void { const { uri, hostname } = props.server; connectToServer( @@ -97,7 +92,7 @@ export function ConnectServerActionButton(props: { const commonProps = { inputType: MenuInputType.FILTER, textTransform: 'none', - getLoginItems: () => getSshLogins().map(login => ({ login, url: '' })), + getLoginItems: () => props.server.logins.map(login => ({ login, url: '' })), onSelect: (e, login) => connect(login), transformOrigin: { vertical: 'top', diff --git a/web/packages/teleterm/src/ui/Search/actions.tsx b/web/packages/teleterm/src/ui/Search/actions.tsx index f8bd045aec7dd..13a89cbfd2b1d 100644 --- a/web/packages/teleterm/src/ui/Search/actions.tsx +++ b/web/packages/teleterm/src/ui/Search/actions.tsx @@ -91,15 +91,11 @@ export function mapToAction( type: 'parametrized-action', searchResult: result, parameter: { - getSuggestions: async () => { - const sshLogins = ctx.clustersService.findClusterByResource( - result.resource.uri - )?.loggedInUser?.sshLogins; - return sshLogins?.map(login => ({ + getSuggestions: async () => + result.resource.logins.map(login => ({ value: login, displayText: login, - })); - }, + })), placeholder: 'Provide login', }, perform: login => { diff --git a/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.story.tsx b/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.story.tsx index 8a4d827e6e4b0..a142f2855aa06 100644 --- a/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.story.tsx +++ b/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.story.tsx @@ -36,7 +36,6 @@ const clusterOrange = makeRootCluster({ loggedInUser: makeLoggedInUser({ name: 'bob', roles: ['access', 'editor'], - sshLogins: ['root'], }), uri: '/clusters/orange', }); diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx index 43eb4967ef282..4a4bcca868a7c 100644 --- a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx +++ b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx @@ -41,7 +41,6 @@ const clusterOrange = makeRootCluster({ loggedInUser: makeLoggedInUser({ name: 'bob', roles: ['access', 'editor'], - sshLogins: ['root'], }), uri: '/clusters/orange', }); @@ -117,7 +116,6 @@ export function OneClusterWithActiveCluster() { loggedInUser: makeLoggedInUser({ name: 'alice', roles: ['access', 'editor'], - sshLogins: ['root'], }), }); @@ -197,7 +195,6 @@ export function TrustedDeviceEnrolled() { loggedInUser: makeLoggedInUser({ isDeviceTrusted: true, roles: ['circle-mark-app-access', 'grafana-lite-app-access'], - sshLogins: ['root'], }), }), ]} @@ -216,7 +213,6 @@ export function TrustedDeviceRequiredButNotEnrolled() { loggedInUser: makeLoggedInUser({ trustedDeviceRequirement: TrustedDeviceRequirement.REQUIRED, roles: ['circle-mark-app-access', 'grafana-lite-app-access'], - sshLogins: ['root'], }), }), ]} diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index 02832b20d22f8..9e9afbc49fd89 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -31,7 +31,6 @@ import { } from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb'; import { useStore } from 'shared/libs/stores'; import { isAbortError } from 'shared/utils/error'; -import { pipe } from 'shared/utils/pipe'; import { MainProcessClient } from 'teleterm/mainProcess/types'; import { cloneAbortSignal, TshdClient } from 'teleterm/services/tshd'; @@ -86,10 +85,7 @@ export class ClustersService extends ImmutableStore { // fetched from the auth server at the RPC message level. if (!this.state.clusters.has(cluster.uri)) { this.setState(draft => { - draft.clusters.set( - cluster.uri, - this.removeInternalLoginsFromCluster(cluster) - ); + draft.clusters.set(cluster.uri, cluster); }); } @@ -237,9 +233,7 @@ export class ClustersService extends ImmutableStore { } this.setState(draft => { - draft.clusters = new Map( - clusters.map(c => [c.uri, this.removeInternalLoginsFromCluster(c)]) - ); + draft.clusters = new Map(clusters.map(c => [c.uri, c])); }); // Sync root clusters and resume headless watchers for any active login sessions. @@ -276,10 +270,7 @@ export class ClustersService extends ImmutableStore { this.setState(draft => { for (const leaf of response.clusters) { - draft.clusters.set( - leaf.uri, - this.removeInternalLoginsFromCluster(leaf) - ); + draft.clusters.set(leaf.uri, leaf); } }); @@ -591,13 +582,9 @@ export class ClustersService extends ImmutableStore { assumedRequests, }, }); - const processCluster = pipe( - this.removeInternalLoginsFromCluster, - mergeAssumedRequests - ); this.setState(draft => { - draft.clusters.set(clusterUri, processCluster(cluster)); + draft.clusters.set(clusterUri, mergeAssumedRequests(cluster)); }); } catch (error) { this.setState(draft => { @@ -630,21 +617,6 @@ export class ClustersService extends ImmutableStore { return requestsMap; }, {}); } - - // temporary fix for https://github.com/gravitational/webapps.e/issues/294 - // remove when it will get fixed in `tsh` - // alternatively, show only valid logins basing on RBAC check - private removeInternalLoginsFromCluster(cluster: Cluster): Cluster { - return { - ...cluster, - loggedInUser: cluster.loggedInUser && { - ...cluster.loggedInUser, - sshLogins: cluster.loggedInUser.sshLogins.filter( - login => !login.startsWith('-') - ), - }, - }; - } } // A workaround to always return the same object so useEffect that relies on it