Skip to content

Commit

Permalink
Merge branch 'branch/v17' into bot/backport-50363-branch/v17
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielcorado authored Dec 25, 2024
2 parents 6f6dcf8 + 5100649 commit e18383d
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 116 deletions.
13 changes: 0 additions & 13 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5032,19 +5032,6 @@ func findActiveApps(keyRing *KeyRing) ([]tlsca.RouteToApp, error) {
return apps, nil
}

// getDesktopEventWebURL returns the web UI URL users can access to
// watch a desktop session recording in the browser
func getDesktopEventWebURL(proxyHost string, cluster string, sid *session.ID, events []events.EventFields) string {
if len(events) < 1 {
return ""
}
start := events[0].GetTimestamp()
end := events[len(events)-1].GetTimestamp()
duration := end.Sub(start)

return fmt.Sprintf("https://%s/web/cluster/%s/session/%s?recordingType=desktop&durationMs=%d", proxyHost, cluster, sid, duration/time.Millisecond)
}

// SearchSessionEvents allows searching for session events with a full pagination support.
func (tc *TeleportClient) SearchSessionEvents(ctx context.Context, fromUTC, toUTC time.Time, pageSize int, order types.EventOrder, max int) ([]apievents.AuditEvent, error) {
ctx, span := tc.Tracer.Start(
Expand Down
75 changes: 0 additions & 75 deletions lib/client/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"math"
"os"
"testing"
"time"

"github.com/coreos/go-semver/semver"
"github.com/google/go-cmp/cmp"
Expand All @@ -45,10 +44,8 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"
)

Expand Down Expand Up @@ -929,78 +926,6 @@ func TestFormatConnectToProxyErr(t *testing.T) {
}
}

func TestGetDesktopEventWebURL(t *testing.T) {
initDate := time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC)

tt := []struct {
name string
proxyHost string
cluster string
sid session.ID
events []events.EventFields
expected string
}{
{
name: "nil events",
events: nil,
expected: "",
},
{
name: "empty events",
events: make([]events.EventFields, 0),
expected: "",
},
{
name: "two events, 1000 ms duration",
proxyHost: "host",
cluster: "cluster",
sid: "session_id",
events: []events.EventFields{
{
"time": initDate,
},
{
"time": initDate.Add(1000 * time.Millisecond),
},
},
expected: "https://host/web/cluster/cluster/session/session_id?recordingType=desktop&durationMs=1000",
},
{
name: "multiple events",
proxyHost: "host",
cluster: "cluster",
sid: "session_id",
events: []events.EventFields{
{
"time": initDate,
},
{
"time": initDate.Add(10 * time.Millisecond),
},
{
"time": initDate.Add(20 * time.Millisecond),
},
{
"time": initDate.Add(30 * time.Millisecond),
},
{
"time": initDate.Add(40 * time.Millisecond),
},
{
"time": initDate.Add(50 * time.Millisecond),
},
},
expected: "https://host/web/cluster/cluster/session/session_id?recordingType=desktop&durationMs=50",
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, getDesktopEventWebURL(tc.proxyHost, tc.cluster, &tc.sid, tc.events))
})
}
}

type mockRoleGetter func(ctx context.Context) ([]types.Role, error)

func (m mockRoleGetter) GetRoles(ctx context.Context) ([]types.Role, error) {
Expand Down
1 change: 1 addition & 0 deletions lib/events/auditlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ func (l *AuditLog) StreamSessionEvents(ctx context.Context, sessionID session.ID
}

protoReader := NewProtoReader(rawSession)
defer protoReader.Close()

for {
if ctx.Err() != nil {
Expand Down
1 change: 1 addition & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/sites/:site/events/search", h.WithClusterAuth(h.clusterSearchEvents)) // search site events
h.GET("/webapi/sites/:site/events/search/sessions", h.WithClusterAuth(h.clusterSearchSessionEvents)) // search site session events
h.GET("/webapi/sites/:site/ttyplayback/:sid", h.WithClusterAuth(h.ttyPlaybackHandle))
h.GET("/webapi/sites/:site/sessionlength/:sid", h.WithClusterAuth(h.sessionLengthHandle))

// scp file transfer
h.GET("/webapi/sites/:site/nodes/:server/:login/scp", h.WithClusterAuth(h.transferFile))
Expand Down
41 changes: 41 additions & 0 deletions lib/web/tty_playback.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,47 @@ const (
actionPause = byte(1)
)

func (h *Handler) sessionLengthHandle(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
sctx *SessionContext,
site reversetunnelclient.RemoteSite,
) (interface{}, error) {
sID := p.ByName("sid")
if sID == "" {
return nil, trace.BadParameter("missing session ID in request URL")
}

ctx, cancel := context.WithCancel(r.Context())
defer cancel()

clt, err := sctx.GetUserClient(ctx, site)
if err != nil {
return nil, trace.Wrap(err)
}

evts, errs := clt.StreamSessionEvents(ctx, session.ID(sID), 0)
for {
select {
case err := <-errs:
return nil, trace.Wrap(err)
case evt, ok := <-evts:
if !ok {
return nil, trace.NotFound("could not find end event for session %v", sID)
}
switch evt := evt.(type) {
case *events.SessionEnd:
return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil
case *events.WindowsDesktopSessionEnd:
return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil
case *events.DatabaseSessionEnd:
return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil
}
}
}
}

func (h *Handler) ttyPlaybackHandle(
w http.ResponseWriter,
r *http.Request,
Expand Down
68 changes: 56 additions & 12 deletions web/packages/teleport/src/Player/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { useCallback, useEffect } from 'react';
import styled from 'styled-components';

import { Flex, Box } from 'design';

import { Flex, Box, Indicator } from 'design';
import { Danger } from 'design/Alert';

import { useParams, useLocation } from 'teleport/components/Router';
import { makeSuccessAttempt, useAsync } from 'shared/hooks/useAsync';

import { useParams, useLocation } from 'teleport/components/Router';
import session from 'teleport/services/websession';
import { UrlPlayerParams } from 'teleport/config';
import { getUrlParameter } from 'teleport/services/history';

import { RecordingType } from 'teleport/services/recordings';

import useTeleport from 'teleport/useTeleport';

import ActionBar from './ActionBar';
import { DesktopPlayer } from './DesktopPlayer';
import SshPlayer from './SshPlayer';
Expand All @@ -38,19 +40,44 @@ import Tabs, { TabItem } from './PlayerTabs';
const validRecordingTypes = ['ssh', 'k8s', 'desktop', 'database'];

export function Player() {
const ctx = useTeleport();
const { sid, clusterId } = useParams<UrlPlayerParams>();
const { search } = useLocation();

useEffect(() => {
document.title = `Play ${sid}${clusterId}`;
}, [sid, clusterId]);

const recordingType = getUrlParameter(
'recordingType',
search
) as RecordingType;
const durationMs = Number(getUrlParameter('durationMs', search));

// In order to render the progress bar, we need to know the length of the session.
// All in-product links to the session player should include the session duration in the URL.
// Some users manually build the URL based on the session ID and don't specify the session duration.
// For those cases, we make a separate API call to get the duration.
const [fetchDurationAttempt, fetchDuration] = useAsync(
useCallback(
() => ctx.recordingsService.fetchRecordingDuration(clusterId, sid),
[ctx.recordingsService, clusterId, sid]
)
);

const validRecordingType = validRecordingTypes.includes(recordingType);
const validDurationMs = Number.isInteger(durationMs) && durationMs > 0;
const durationMs = Number(getUrlParameter('durationMs', search));
const shouldFetchSessionDuration =
validRecordingType && (!Number.isInteger(durationMs) || durationMs <= 0);

useEffect(() => {
if (shouldFetchSessionDuration) {
fetchDuration();
}
}, [fetchDuration, shouldFetchSessionDuration]);

document.title = `Play ${sid}${clusterId}`;
const combinedAttempt = shouldFetchSessionDuration
? fetchDurationAttempt
: makeSuccessAttempt({ durationMs });

function onLogout() {
session.logout();
Expand All @@ -69,13 +96,25 @@ export function Player() {
);
}

if (!validDurationMs) {
if (
combinedAttempt.status === '' ||
combinedAttempt.status === 'processing'
) {
return (
<StyledPlayer>
<Box textAlign="center" mx={10} mt={5}>
<Indicator />
</Box>
</StyledPlayer>
);
}
if (combinedAttempt.status === 'error') {
return (
<StyledPlayer>
<Box textAlign="center" mx={10} mt={5}>
<Danger mb={0}>
Invalid query parameter durationMs:{' '}
{getUrlParameter('durationMs', search)}, should be an integer.
Unable to determine the length of this session. The session
recording may be incomplete or corrupted.
</Danger>
</Box>
</StyledPlayer>
Expand All @@ -101,15 +140,20 @@ export function Player() {
<DesktopPlayer
sid={sid}
clusterId={clusterId}
durationMs={durationMs}
durationMs={combinedAttempt.data.durationMs}
/>
) : (
<SshPlayer sid={sid} clusterId={clusterId} durationMs={durationMs} />
<SshPlayer
sid={sid}
clusterId={clusterId}
durationMs={combinedAttempt.data.durationMs}
/>
)}
</Flex>
</StyledPlayer>
);
}

const StyledPlayer = styled.div`
display: flex;
height: 100%;
Expand Down
35 changes: 21 additions & 14 deletions web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,36 @@
*/

import { generatePath } from 'react-router';
import { mergeDeep } from 'shared/utils/highbar';
import { IncludedResourceMode } from 'shared/components/UnifiedResources';

import generateResourcePath from './generateResourcePath';
import { IncludedResourceMode } from 'shared/components/UnifiedResources';

import { defaultEntitlements } from './entitlement';
import { mergeDeep } from 'shared/utils/highbar';

import {
AwsOidcPolicyPreset,
IntegrationKind,
PluginKind,
Regions,
} from './services/integrations';

import type {
Auth2faType,
AuthProvider,
AuthType,
PreferredMfaType,
PrimaryAuthType,
} from 'shared/services';

import {
AwsOidcPolicyPreset,
IntegrationKind,
PluginKind,
Regions,
} from 'teleport/services/integrations';
import { KubeResourceKind } from 'teleport/services/kube/types';

import { defaultEntitlements } from './entitlement';

import generateResourcePath from './generateResourcePath';

import type { SortType } from 'teleport/services/agents';
import type { RecordingType } from 'teleport/services/recordings';
import type { WebauthnAssertionResponse } from './services/mfa';
import type { WebauthnAssertionResponse } from 'teleport/services/mfa';
import type { ParticipantMode } from 'teleport/services/session';
import type { YamlSupportedResourceKind } from './services/yaml/types';
import type { KubeResourceKind } from './services/kube/types';
import type { YamlSupportedResourceKind } from 'teleport/services/yaml/types';

const cfg = {
/** @deprecated Use cfg.edition instead. */
Expand Down Expand Up @@ -268,6 +270,7 @@ const cfg = {
ttyPlaybackWsAddr:
'wss://:fqdn/v1/webapi/sites/:clusterId/ttyplayback/:sid?access_token=:token', // TODO(zmb3): get token out of URL
activeAndPendingSessionsPath: '/v1/webapi/sites/:clusterId/sessions',
sessionDurationPath: '/v1/webapi/sites/:clusterId/sessionlength/:sid',

kubernetesPath:
'/v1/webapi/sites/:clusterId/kubernetes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',
Expand Down Expand Up @@ -778,6 +781,10 @@ const cfg = {
return generatePath(cfg.api.activeAndPendingSessionsPath, { clusterId });
},

getSessionDurationUrl(clusterId: string, sid: string) {
return generatePath(cfg.api.sessionDurationPath, { clusterId, sid });
},

getUnifiedResourcesUrl(clusterId: string, params: UrlResourcesParams) {
return generateResourcePath(cfg.api.unifiedResourcesPath, {
clusterId,
Expand Down
3 changes: 1 addition & 2 deletions web/packages/teleport/src/services/mfa/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

import { AuthProviderType } from 'shared/services';

import { Base64urlString } from '../auth/types';
import { CreateNewHardwareDeviceRequest } from '../auth/types';
import { Base64urlString, CreateNewHardwareDeviceRequest } from '../auth/types';

export type DeviceType = 'totp' | 'webauthn' | 'sso';

Expand Down
Loading

0 comments on commit e18383d

Please sign in to comment.