From 91c4efaecba1c97ffe942c5eee3c697b613798de Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Fri, 28 Apr 2023 13:49:11 -0700 Subject: [PATCH] webapi cleanup (#24363) * Add isModeratedSession flag to web ssh session * Fix lint * Change to snakecase * Change to moderated * Ensures every Session has a valid Kind field. Also cleans up the client code to distinguish between terminalSessionPath and activeAndPendingSessionsPath. * Entirely removes siteSessionGet which was not being used * removes siteSessionGenerate which was not being used * removes test testing webapi/sites//sessions post call, which is no longer used by the frontend * removing unused struct --------- Co-authored-by: Michael Myers --- lib/auth/auth_with_roles.go | 1 + lib/session/session.go | 7 +- lib/srv/sess.go | 3 +- lib/web/apiserver.go | 94 ++----------------- lib/web/apiserver_test.go | 44 --------- .../teleport/src/Player/SshPlayer.tsx | 4 +- web/packages/teleport/src/config.ts | 12 ++- .../teleport/src/services/session/session.ts | 40 ++++---- 8 files changed, 45 insertions(+), 160 deletions(-) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index c194681190723..18ab0f4662ed7 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -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(), diff --git a/lib/session/session.go b/lib/session/session.go index fdf6960e85e08..b9cd613fdbb3d 100644 --- a/lib/session/session.go +++ b/lib/session/session.go @@ -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"` @@ -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"` } diff --git a/lib/srv/sess.go b/lib/srv/sess.go index ccfd3b226d2a9..f503a16d57c34 100644 --- a/lib/srv/sess.go +++ b/lib/srv/sess.go @@ -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, diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 9a8b31d4bd502..d5914fbe8bd92 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -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 @@ -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, @@ -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 { @@ -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"` } @@ -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) @@ -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) { diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 4d8799d94ce8f..abb1276a90502 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -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) diff --git a/web/packages/teleport/src/Player/SshPlayer.tsx b/web/packages/teleport/src/Player/SshPlayer.tsx index 00c82fcc1aa63..75a2efb7c9dac 100644 --- a/web/packages/teleport/src/Player/SshPlayer.tsx +++ b/web/packages/teleport/src/Player/SshPlayer.tsx @@ -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 diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index af109ffd031fa..b8a2bb054f846 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -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¶ms=: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?', @@ -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) { diff --git a/web/packages/teleport/src/services/session/session.ts b/web/packages/teleport/src/services/session/session.ts index 2ae674d3e5223..f132126f36fe3 100644 --- a/web/packages/teleport/src/services/session/session.ts +++ b/web/packages/teleport/src/services/session/session.ts @@ -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; - }); }, };