Files
teleport/lib/web/command_test.go
T
rosstimothy 553eb02bdf Fix moderated session presence checking (#25988)
* Fix moderated session presence checking

Addresses all of the issues that were preventing presence checking during
moderated sessions from working as described in
[#18092](https://github.com/gravitational/teleport/issues/18092#issuecomment-1540900859).

Closes #18092

* make presence test clearer

* fix presence checking on the web ui

* Refactor web socket message handling

A single message processing loop handles retrieving the envelope
and passing it off to individual message handlers. This allows all
messages to be processed outside of `Read` which was dependent on
the terminal being active to process any messages.

The webauthn challenge response was also moved from a raw message
to a webauthn message. By sending it as a raw message it made
presence checking fail because the response has a `t` in it which
caused the session to be killed during moderated sessions.

* enforce mfa ceremony when joining and cluster wide mfa is enabled

* fix conflicts with master

* moderated tests

* Add moderated session tests for the UI

* Add moderated session integration test

* fix lints

* clean up presence test

* refactor envelope handling

* fix build

* fix: revert test debug timeout

* fix: use local context in tests

* simplify closing streams

* generalize waitForOutput to work with an io.Reader

* fix error handling in stream close

* unexport PresenceOptions

* Improve waitForOutput to match against output in successive reads
2023-06-08 20:27:13 +00:00

187 lines
4.4 KiB
Go

/*
* Copyright 2023 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package web
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/gogo/protobuf/proto"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"
)
func TestExecuteCommand(t *testing.T) {
t.Parallel()
s := newWebSuite(t)
ws, _, err := s.makeCommand(t, s.authPack(t, "foo"))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, ws.Close()) })
stream := NewWStream(context.Background(), ws, utils.NewLoggerForTests(), nil)
require.NoError(t, waitForCommandOutput(stream, "teleport"))
}
func (s *WebSuite) makeCommand(t *testing.T, pack *authPack) (*websocket.Conn, *session.Session, error) {
req := CommandRequest{
Query: fmt.Sprintf("name == \"%s\"", s.srvID),
Login: pack.login,
ConversationID: uuid.New().String(),
ExecutionID: uuid.New().String(),
Command: "echo txlxport | sed 's/x/e/g'",
}
u := url.URL{
Host: s.url().Host,
Scheme: client.WSS,
Path: fmt.Sprintf("/v1/webapi/command/%v/execute", currentSiteShortcut),
}
data, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
q := u.Query()
q.Set("params", string(data))
q.Set(roundtrip.AccessTokenQueryParam, pack.session.Token)
u.RawQuery = q.Encode()
dialer := websocket.Dialer{}
dialer.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
header := http.Header{}
header.Add("Origin", "http://localhost")
for _, cookie := range pack.cookies {
header.Add("Cookie", cookie.String())
}
ws, resp, err := dialer.Dial(u.String(), header)
if err != nil {
return nil, nil, trace.Wrap(err)
}
ty, raw, err := ws.ReadMessage()
if err != nil {
return nil, nil, trace.Wrap(err)
}
require.Equal(t, websocket.BinaryMessage, ty)
var env Envelope
err = proto.Unmarshal(raw, &env)
if err != nil {
return nil, nil, trace.Wrap(err)
}
var sessResp siteSessionGenerateResponse
err = json.Unmarshal([]byte(env.Payload), &sessResp)
if err != nil {
return nil, nil, trace.Wrap(err)
}
err = resp.Body.Close()
if err != nil {
return nil, nil, trace.Wrap(err)
}
return ws, &sessResp.Session, nil
}
func waitForCommandOutput(stream io.Reader, substr string) error {
timeoutCh := time.After(10 * time.Second)
for {
select {
case <-timeoutCh:
return trace.BadParameter("timeout waiting on terminal for output: %v", substr)
default:
}
out := make([]byte, 100)
n, err := stream.Read(out)
if err != nil {
return trace.Wrap(err)
}
var env Envelope
err = json.Unmarshal(out[:n], &env)
if err != nil {
return trace.Wrap(err)
}
d, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return trace.Wrap(err)
}
data := removeSpace(string(d))
if n > 0 && strings.Contains(data, substr) {
return nil
}
if err != nil {
return trace.Wrap(err)
}
}
}
// Test_runCommands tests that runCommands runs the given command on all hosts.
// The commands should run in parallel, but we don't have a deterministic way to
// test that (sleep with checking the execution time in not deterministic).
func Test_runCommands(t *testing.T) {
counter := atomic.Int32{}
runCmd := func(host *hostInfo) error {
counter.Add(1)
return nil
}
hosts := make([]hostInfo, 0, 100)
for i := 0; i < 100; i++ {
hosts = append(hosts, hostInfo{
hostName: fmt.Sprintf("localhost%d", i),
})
}
logger := logrus.New()
logger.Out = io.Discard
runCommands(hosts, runCmd, logger)
require.Equal(t, int32(100), counter.Load())
}