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
1 change: 1 addition & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ func (a *ServerWithRoles) filterSessionTracker(ctx context.Context, joinerRoles
if tracker.GetKind() == types.KindSSHSession {
ruleCtx := &services.Context{User: a.context.User}
ruleCtx.SSHSession = &session.Session{
Kind: tracker.GetSessionKind(),
ID: session.ID(tracker.GetSessionID()),
Namespace: apidefaults.Namespace,
Login: tracker.GetLogin(),
Expand Down
7 changes: 3 additions & 4 deletions lib/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ func NewID() ID {
return ID(uuid.New().String())
}

// Session is an interactive collaboration session that represents one
// or many sessions started by the teleport user.
// Session is a session of any kind (SSH, Kubernetes, Desktop, etc)
type Session struct {
// Kind describes what kind of session this is e.g. ssh or kubernetes.
// Kind describes what kind of session this is e.g. ssh or k8s.
Kind types.SessionKind `json:"kind"`
// ID is a unique session identifier
ID ID `json:"id"`
Expand Down Expand Up @@ -106,7 +105,7 @@ type Session struct {
AppName string `json:"app_name"`
// Owner is the name of the session owner, ie the one who created the session.
Owner string `json:"owner"`
// Moderated is true if the session requires moderation.
// Moderated is true if the session requires moderation (only relevant for Kind = ssh/k8s).
Moderated bool `json:"moderated"`
}

Expand Down
3 changes: 2 additions & 1 deletion lib/srv/sess.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,8 @@ func newSession(ctx context.Context, id rsession.ID, r *SessionRegistry, scx *Se
serverSessions.Inc()
startTime := time.Now().UTC()
rsess := rsession.Session{
ID: id,
Kind: types.SSHSessionKind,
ID: id,
TerminalParams: rsession.TerminalParams{
W: teleport.DefaultTerminalWidth,
H: teleport.DefaultTerminalHeight,
Expand Down
94 changes: 7 additions & 87 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,8 @@ func (h *Handler) bindDefaultEndpoints() {
h.DELETE("/webapi/sites/:site/locks/:uuid", h.WithClusterAuth(h.deleteClusterLock))

// active sessions handlers
h.GET("/webapi/sites/:site/connect", h.WithClusterAuth(h.siteNodeConnect)) // connect to an active session (via websocket)
h.GET("/webapi/sites/:site/sessions", h.WithClusterAuth(h.siteSessionsGet)) // get active list of sessions
// TODO POSTS to `/webapi/sites/:site/sessions` should no longer be required
// but this endpoint is still used by the UI. When time allows evaluate the
// removal of this handler and the associated methods here and in the UI.
h.POST("/webapi/sites/:site/sessions", h.WithClusterAuth(h.siteSessionGenerate)) // create active session metadata
h.GET("/webapi/sites/:site/sessions/:sid", h.WithClusterAuth(h.siteSessionGet)) // get active session metadata
h.GET("/webapi/sites/:site/connect", h.WithClusterAuth(h.siteNodeConnect)) // connect to an active session (via websocket)
h.GET("/webapi/sites/:site/sessions", h.WithClusterAuth(h.clusterActiveAndPendingSessionsGet)) // get list of active and pending sessions

// Audit events handlers.
h.GET("/webapi/sites/:site/events/search", h.WithClusterAuth(h.clusterSearchEvents)) // search site events
Expand Down Expand Up @@ -2747,6 +2742,7 @@ func (h *Handler) generateSession(ctx context.Context, clt auth.ClientI, req *Te
accessEvaluator := auth.NewSessionAccessEvaluator(policySets, types.SSHSessionKind, owner)

return session.Session{
Kind: types.SSHSessionKind,
Login: req.Login,
ServerID: id,
ClusterName: clusterName,
Expand All @@ -2761,6 +2757,7 @@ func (h *Handler) generateSession(ctx context.Context, clt auth.ClientI, req *Te
}, nil
}

// fetchExistingSession fetches an active or pending SSH session by the SessionID passed in the TerminalRequest.
func (h *Handler) fetchExistingSession(ctx context.Context, clt auth.ClientI, req *TerminalRequest, siteName string) (session.Session, string, error) {
sessionID, err := session.ParseID(req.SessionID.String())
if err != nil {
Expand Down Expand Up @@ -2792,51 +2789,10 @@ func (h *Handler) fetchExistingSession(ctx context.Context, clt auth.ClientI, re
return sessionData, displayLogin, nil
}

type siteSessionGenerateReq struct {
Session session.Session `json:"session"`
}

type siteSessionGenerateResponse struct {
Session session.Session `json:"session"`
}

// siteSessionCreate generates a new site session that can be used by UI
// The ServerID from request can be in the form of hostname, uuid, or ip address.
func (h *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}

var req *siteSessionGenerateReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

namespace := apidefaults.Namespace
if req.Session.ServerID != "" {
servers, err := clt.GetNodes(r.Context(), namespace)
if err != nil {
return nil, trace.Wrap(err)
}

hostname, _, err := resolveServerHostPort(req.Session.ServerID, servers)
if err != nil {
return nil, trace.Wrap(err)
}

req.Session.Kind = types.SSHSessionKind
req.Session.ServerHostname = hostname
}

req.Session.ID = session.NewID()
req.Session.Created = time.Now().UTC()
req.Session.LastActive = time.Now().UTC()
req.Session.Namespace = namespace

return siteSessionGenerateResponse{Session: req.Session}, nil
}

type siteSessionsGetResponse struct {
Sessions []siteSessionsGetResponseSession `json:"sessions"`
}
Expand Down Expand Up @@ -2886,15 +2842,10 @@ func trackerToLegacySession(tracker types.SessionTracker, clusterName string) se
}
}

// siteSessionsGet gets the list of site sessions filtered by creation time
// and whether they're active or not
// clusterActiveAndPendingSessionsGet gets the list of active and pending sessions for a site.
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions
//
// Response body:
//
// {"sessions": [{"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}, ...] }
func (h *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
// GET /v1/webapi/sites/:site/sessions
func (h *Handler) clusterActiveAndPendingSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -2929,37 +2880,6 @@ func (h *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p http
return siteSessionsGetResponse{Sessions: sessions}, nil
}

// siteSessionGet gets the list of site session by id
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions/:sid
//
// Response body:
//
// {"session": {"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}}
func (h *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
}
h.log.Infof("web.getSession(%v)", sessionID)

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

tracker, err := clt.GetSessionTracker(r.Context(), string(*sessionID))
if err != nil {
return nil, trace.Wrap(err)
}

if tracker.GetSessionKind() != types.SSHSessionKind || tracker.GetState() == types.SessionState_SessionStateTerminated {
return nil, trace.NotFound("session %v not found", sessionID)
}

return trackerToLegacySession(tracker, site.GetName()), nil
}

const maxStreamBytes = 5 * 1024 * 1024

func toFieldsSlice(rawEvents []apievents.AuditEvent) ([]events.EventFields, error) {
Expand Down
44 changes: 0 additions & 44 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2055,50 +2055,6 @@ func TestCloseConnectionsOnLogout(t *testing.T) {
}
}

func TestCreateSession(t *testing.T) {
t.Parallel()
env := newWebPack(t, 1)
proxy := env.proxies[0]
user := "test-user@example.com"
pack := proxy.authPack(t, user, nil /* roles */)

// get site nodes
re, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "nodes"), url.Values{})
require.NoError(t, err)

nodes := clusterNodesGetResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &nodes))
node := nodes.Items[0]

sess := session.Session{
TerminalParams: session.TerminalParams{W: 300, H: 120},
Login: user,
}

// test using node UUID
sess.ServerID = node.Name
re, err = pack.clt.PostJSON(
context.Background(),
pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "sessions"),
siteSessionGenerateReq{Session: sess},
)
require.NoError(t, err)

var created *siteSessionGenerateResponse
require.NoError(t, json.Unmarshal(re.Bytes(), &created))
require.NotEmpty(t, created.Session.ID)
require.Equal(t, node.Hostname, created.Session.ServerHostname)

// test empty serverID (older version does not supply serverID)
sess.ServerID = ""
_, err = pack.clt.PostJSON(
context.Background(),
pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "sessions"),
siteSessionGenerateReq{Session: sess},
)
require.NoError(t, err)
}

func TestPlayback(t *testing.T) {
t.Parallel()
s := newWebSuite(t)
Expand Down
4 changes: 2 additions & 2 deletions web/packages/teleport/src/Player/SshPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ const StyledPlayer = styled.div`

function useSshPlayer(clusterId: string, sid: string) {
const tty = React.useMemo(() => {
const url = cfg.getTerminalSessionUrl({ clusterId, sid });
return new TtyPlayer(new EventProvider({ url }));
const prefixUrl = cfg.getSshPlaybackPrefixUrl({ clusterId, sid });
return new TtyPlayer(new EventProvider({ url: prefixUrl }));
}, [sid, clusterId]);

// to trigger re-render when tty state changes
Expand Down
12 changes: 8 additions & 4 deletions web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ const cfg = {
desktopPlaybackWsAddr:
'wss://:fqdn/v1/webapi/sites/:clusterId/desktopplayback/:sid?access_token=:token',
desktopIsActive: '/v1/webapi/sites/:clusterId/desktops/:desktopName/active',
siteSessionPath: '/v1/webapi/sites/:siteId/sessions',
ttyWsAddr:
'wss://:fqdn/v1/webapi/sites/:clusterId/connect?access_token=:token&params=:params&traceparent=:traceparent',
terminalSessionPath: '/v1/webapi/sites/:clusterId/sessions/:sid?',
activeAndPendingSessionsPath: '/v1/webapi/sites/:clusterId/sessions',
sshPlaybackPrefix: '/v1/webapi/sites/:clusterId/sessions/:sid', // prefix because this is eventually concatenated with "/stream" or "/events"
kubernetesPath:
'/v1/webapi/sites/:clusterId/kubernetes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',

Expand Down Expand Up @@ -472,8 +472,12 @@ const cfg = {
return generatePath(cfg.api.userWithUsernamePath, { username });
},

getTerminalSessionUrl({ clusterId, sid }: UrlParams) {
return generatePath(cfg.api.terminalSessionPath, { clusterId, sid });
getSshPlaybackPrefixUrl({ clusterId, sid }: UrlParams) {
return generatePath(cfg.api.sshPlaybackPrefix, { clusterId, sid });
},

getActiveAndPendingSessionsUrl({ clusterId }: UrlParams) {
return generatePath(cfg.api.activeAndPendingSessionsPath, { clusterId });
},

getClusterNodesUrl(clusterId: string, params: UrlResourcesParams) {
Expand Down
40 changes: 22 additions & 18 deletions web/packages/teleport/src/services/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,35 @@ import { ParticipantList } from './types';

const service = {
fetchSessions(clusterId) {
return api.get(cfg.getTerminalSessionUrl({ clusterId })).then(response => {
if (response && response.sessions) {
return response.sessions.map(makeSession);
}

return [];
});
return api
.get(cfg.getActiveAndPendingSessionsUrl({ clusterId }))
.then(response => {
if (response && response.sessions) {
return response.sessions.map(makeSession);
}

return [];
});
},

fetchParticipants({ clusterId }: { clusterId: string }) {
// Because given session might not be available right away,
// we query for all active session to find this session participants.
// This is to avoid 404 errors.
return api.get(cfg.getTerminalSessionUrl({ clusterId })).then(json => {
if (!json && !json.sessions) {
return {};
}

const parties: ParticipantList = {};
json.sessions.forEach(s => {
parties[s.id] = s.parties.map(makeParticipant);
return api
.get(cfg.getActiveAndPendingSessionsUrl({ clusterId }))
.then(json => {
if (!json && !json.sessions) {
return {};
}

const parties: ParticipantList = {};
json.sessions.forEach(s => {
parties[s.id] = s.parties.map(makeParticipant);
});

return parties;
});

return parties;
});
},
};

Expand Down