diff --git a/docs/testplan.md b/docs/testplan.md index 6b3f39ccbfd85..4f620b1ff7d83 100644 --- a/docs/testplan.md +++ b/docs/testplan.md @@ -895,3 +895,86 @@ and non interactive tsh bench loads. - [ ] Multiple sessions as different users on the same desktop are allowed. - [ ] Connect multiple `windows_desktop_service`s to the same Teleport cluster, verify that connections to desktops on different AD domains works. + verify that connections to desktops on different AD domains works. (Attempt to + connect several times to verify that you are routed to the correct + `windows_desktop_service`) +- Verify user input + - [ ] Download [Keyboard Key Info](https://dennisbabkin.com/kbdkeyinfo/) and + verify all keys are processed correctly in each supported browser. Known + issues: F11 cannot be captured by the browser without + [special configuration](https://social.technet.microsoft.com/Forums/en-US/784b2bbe-353f-412e-ac9a-193d81f306b6/remote-desktop-for-mac-f11-key-not-working-on-macbook-pro-touchbar?forum=winRDc) + on MacOS. + - [ ] Left click and right click register as Windows clicks. (Right click on + the desktop should show a Windows menu, not a browser context menu) + - [ ] Vertical and horizontal scroll work. + [Horizontal Scroll Test](https://codepen.io/jaemskyle/pen/inbmB) +- Locking + - [ ] Verify that placing a user lock terminates an active desktop session. + - [ ] Verify that placing a desktop lock terminates an active desktop session. + - [ ] Verify that placing a role lock terminates an active desktop session. +- Labeling + - [ ] Set `client_idle_timeout` to a small value and verify that idle sessions + are terminated (the session should end and an audit event will confirm it + was due to idle connection) + - [ ] All desktops have `teleport.dev/origin` label. + - [ ] Dynamic desktops have additional `teleport.dev` labels for OS, OS + Version, DNS hostname. + - [ ] Regexp-based host labeling applies across all desktops, regardless of + origin. +- RBAC + - [ ] RBAC denies access to a Windows desktop due to labels + - [ ] RBAC denies access to a Windows desktop with the wrong OS-login. +- Clipboard Support + - When a user has a role with clipboard sharing enabled and is using a chromium based browser + - [ ] Going to a desktop when clipboard permissions are in "Ask" mode (aka "prompt") causes the browser to show a prompt while the UI shows a spinner + - [ ] X-ing out of the prompt (causing the clipboard permission to remain in "Ask" mode) causes the prompt to show up again + - [ ] Denying clibpoard permissions brings up a relevant error alert (with "Clipboard Sharing Disabled" in the top bar) + - [ ] Allowing clipboard permissions allows you to see the desktop session, with "Clipboard Sharing Enabled" highlighted in the top bar + - [ ] Copy text from local workstation, paste into remote desktop + - [ ] Copy text from remote desktop, paste into local workstation + - When a user has a role with clipboard sharing enabled and is *not* using a chromium based browser + - [ ] The UI shows a relevant alert and "Clipboard Sharing Disabled" is highlighted in the top bar + - When a user has a role with clipboard sharing *disabled* and is using a chromium and non-chromium based browser (confirm both) + - [ ] The live session should show disabled in the top bar and copy/paste should not work between your workstation and the remote desktop. +- Per-Session MFA (try webauthn on each of Chrome, Safari, and Firefox; u2f only works with Firefox) + - [ ] Attempting to start a session no keys registered shows an error message + - [ ] Attempting to start a session with a u2f key registered shows an error message + - [ ] Attempting to start a session with a webauthn registered pops up the "Verify Your Identity" dialog + - [ ] Hitting "Cancel" shows an error message + - [ ] Hitting "Verify" causes your browser to prompt you for MFA + - [ ] Cancelling that browser MFA prompt shows an error + - [ ] Successful MFA verification allows you to connect +- Session Recording + - [ ] Verify sessions are not recorded if *all* of a user's roles disable recording + - [ ] Verify sync recording (`mode: node-sync` or `mode: proy-sync`) + - [ ] Verify async recording (`mode: node` or `mode: proxy`) + - [ ] Sessions show up in session recordings UI with desktop icon + - [ ] Sessions can be played back, including play/pause functionality + - [ ] A session that ends with a TDP error message can be played back, ends by displaying the error message, + and the progress bar progresses to the end. + - [ ] Attempting to play back a session that doesn't exist (i.e. by entering a non-existing session id in the url) shows + a relevant error message. + - [ ] RBAC for sessions: ensure users can only see their own recordings when + using the RBAC rule from our + [docs](../../docs/pages/access-controls/reference.mdx#rbac-for-sessions) +- Audit Events (check these after performing the above tests) + - [ ] `windows.desktop.session.start` (`TDP00I`) emitted on start + - [ ] `windows.desktop.session.start` (`TDP00W`) emitted when session fails to + start (due to RBAC, for example) + - [ ] `windows.desktop.session.end` (`TDP01I`) emitted on end + - [ ] `desktop.clipboard.send` (`TDP02I`) emitted for local copy -> remote + paste + - [ ] `desktop.clipboard.receive` (`TDP03I`) emitted for remote copy -> local + paste + +## Binaries compatibility + +- Verify that teleport/tsh/tctl/tbot run on: + - [ ] CentOS 7 + - [ ] CentOS 8 + - [ ] Ubuntu 18.04 + - [ ] Ubuntu 20.04 + - [ ] Debian 9 +- Verify tsh runs on: + - [ ] Windows 10 + - [ ] MacOS diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index a74d6695b78b8..e298b7ab17739 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -362,13 +362,17 @@ func (c *Client) start() { return } case tdp.ClipboardData: - if err := cgoError(C.update_clipboard( - c.rustClient, - (*C.uint8_t)(unsafe.Pointer(&m[0])), - C.uint32_t(len(m)), - )); err != nil { - c.cfg.Log.Warningf("Failed forwarding RDP clipboard data: %v", err) - return + if len(m) > 0 { + if err := cgoError(C.update_clipboard( + c.rustClient, + (*C.uint8_t)(unsafe.Pointer(&m[0])), + C.uint32_t(len(m)), + )); err != nil { + c.cfg.Log.Warningf("Failed forwarding RDP clipboard data: %v", err) + return + } + } else { + c.cfg.Log.Warning("Recieved an empty clipboard message") } default: c.cfg.Log.Warningf("Skipping unimplemented TDP message type %T", msg) diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index ed33e43a9269b..df4020a428bc1 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -872,7 +872,7 @@ func (s *WindowsService) makeTDPSendHandler(ctx context.Context, emitter events. id *tlsca.Identity, sessionID, desktopAddr string) func(m tdp.Message, b []byte) { return func(m tdp.Message, b []byte) { switch b[0] { - case byte(tdp.TypePNGFrame): + case byte(tdp.TypePNGFrame), byte(tdp.TypeError): e := &events.DesktopRecording{ Metadata: events.Metadata{ Type: libevents.DesktopRecordingEvent, diff --git a/lib/web/desktop/playback.go b/lib/web/desktop/playback.go index 31c72c9cd8785..058dff8c696b3 100644 --- a/lib/web/desktop/playback.go +++ b/lib/web/desktop/playback.go @@ -19,13 +19,16 @@ package desktop import ( "context" "errors" + "fmt" "net" + "os" "sync" "time" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/trace" "github.com/sirupsen/logrus" "golang.org/x/net/websocket" ) @@ -205,6 +208,15 @@ func (pp *Player) streamSessionEvents(ctx context.Context, cancel context.Cancel case err := <-errC: if err != nil && !errors.Is(err, context.Canceled) { pp.log.WithError(err).Errorf("streaming session %v", pp.sID) + var errorText string + if os.IsNotExist(err) || trace.IsNotFound(err) { + errorText = "session not found" + } else { + errorText = "server error" + } + if _, err := pp.ws.Write([]byte(fmt.Sprintf(`{"message": "error", "errorText": "%v"}`, errorText))); err != nil { + pp.log.WithError(err).Error("failed to write \"error\" message over websocket") + } } return case evt := <-eventsC: @@ -225,6 +237,10 @@ func (pp *Player) streamSessionEvents(ctx context.Context, cancel context.Cancel msg, err := utils.FastMarshal(e) if err != nil { pp.log.WithError(err).Errorf("failed to marshal DesktopRecording event into JSON: %v", e) + if _, err := pp.ws.Write([]byte(`{"message":"error","errorText":"server error"}`)); err != nil { + pp.log.WithError(err).Error("failed to write \"error\" message over websocket") + } + return } if _, err := pp.ws.Write(msg); err != nil { // We expect net.ErrClosed to arise when another goroutine returns before diff --git a/lib/web/desktop/playback_test.go b/lib/web/desktop/playback_test.go index bb0be1f908993..3348112580795 100644 --- a/lib/web/desktop/playback_test.go +++ b/lib/web/desktop/playback_test.go @@ -65,7 +65,7 @@ func TestStreamsDesktopEvents(t *testing.T) { b := make([]byte, 4096) n, err := ws.Read(b) require.NoError(t, err) - require.Equal(t, []byte(`{"message":"end"}`), b[:n]) + require.JSONEq(t, `{"message":"end"}`, string(b[:n])) } func newServer(t *testing.T, streamInterval time.Duration, events []apievents.AuditEvent) *httptest.Server {