teleport/lib/web/apiserver.go
2017-09-05 17:05:45 -04:00

1793 lines
54 KiB
Go

/*
Copyright 2015 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 implements web proxy handler that provides
// web interface to view and connect to teleport nodes
package web
import (
"compress/gzip"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/web/ui"
"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/julienschmidt/httprouter"
"github.com/mailgun/lemma/secret"
"github.com/mailgun/ttlmap"
log "github.com/sirupsen/logrus"
"github.com/tstranex/u2f"
)
// Handler is HTTP web proxy handler
type Handler struct {
sync.Mutex
httprouter.Router
cfg Config
auth *sessionCache
sites *ttlmap.TtlMap
sessionStreamPollPeriod time.Duration
clock clockwork.Clock
}
// HandlerOption is a functional argument - an option that can be passed
// to NewHandler function
type HandlerOption func(h *Handler) error
// SetSessionStreamPollPeriod sets polling period for session streams
func SetSessionStreamPollPeriod(period time.Duration) HandlerOption {
return func(h *Handler) error {
if period < 0 {
return trace.BadParameter("period should be non zero")
}
h.sessionStreamPollPeriod = period
return nil
}
}
// Config represents web handler configuration parameters
type Config struct {
// Proxy is a reverse tunnel proxy that handles connections
// to various sites
Proxy reversetunnel.Server
// AuthServers is a list of auth servers this proxy talks to
AuthServers utils.NetAddr
// DomainName is a domain name served by web handler
DomainName string
// ProxyClient is a client that authenticated as proxy
ProxyClient auth.ClientI
// DisableUI allows to turn off serving web based UI
DisableUI bool
// ProxySSHAddr points to the SSH address of the proxy
ProxySSHAddr utils.NetAddr
// ProxyWebAddr points to the web (HTTPS) address of the proxy
ProxyWebAddr utils.NetAddr
}
type RewritingHandler struct {
http.Handler
handler *Handler
}
func (r *RewritingHandler) GetHandler() *Handler {
return r.handler
}
func (r *RewritingHandler) Close() error {
return r.handler.Close()
}
// NewHandler returns a new instance of web proxy handler
func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
const apiPrefix = "/" + teleport.WebAPIVersion
lauth, err := newSessionCache([]utils.NetAddr{cfg.AuthServers})
if err != nil {
return nil, trace.Wrap(err)
}
h := &Handler{
cfg: cfg,
auth: lauth,
}
for _, o := range opts {
if err := o(h); err != nil {
return nil, trace.Wrap(err)
}
}
if h.sessionStreamPollPeriod == 0 {
h.sessionStreamPollPeriod = sessionStreamPollPeriod
}
if h.clock == nil {
h.clock = clockwork.NewRealClock()
}
// ping endpoint is used to check if the server is up. the /webapi/ping
// endpoint returns the default authentication method and configuration that
// the server supports. the /webapi/ping/:connector endpoint can be used to
// query the authentication configuration for a specific connector.
h.GET("/webapi/ping", httplib.MakeHandler(h.ping))
h.GET("/webapi/ping/:connector", httplib.MakeHandler(h.pingWithConnector))
// Web sessions
h.POST("/webapi/sessions", httplib.MakeHandler(h.createSession))
h.DELETE("/webapi/sessions", h.WithAuth(h.deleteSession))
h.POST("/webapi/sessions/renew", h.WithAuth(h.renewSession))
// Users
h.GET("/webapi/users/invites/:token", httplib.MakeHandler(h.renderUserInvite))
h.POST("/webapi/users", httplib.MakeHandler(h.createNewUser))
// Issues SSH temp certificates based on 2FA access creds
h.POST("/webapi/ssh/certs", httplib.MakeHandler(h.createSSHCert))
// list available sites
h.GET("/webapi/sites", h.WithAuth(h.getSites))
// Site specific API
// get namespaces
h.GET("/webapi/sites/:site/namespaces", h.WithClusterAuth(h.getSiteNamespaces))
// get nodes
h.GET("/webapi/sites/:site/namespaces/:namespace/nodes", h.WithClusterAuth(h.siteNodesGet))
// active sessions handlers
h.GET("/webapi/sites/:site/namespaces/:namespace/connect", h.WithClusterAuth(h.siteNodeConnect)) // connect to an active session (via websocket)
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions", h.WithClusterAuth(h.siteSessionsGet)) // get active list of sessions
h.POST("/webapi/sites/:site/namespaces/:namespace/sessions", h.WithClusterAuth(h.siteSessionGenerate)) // create active session metadata
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions/:sid", h.WithClusterAuth(h.siteSessionGet)) // get active session metadata
h.PUT("/webapi/sites/:site/namespaces/:namespace/sessions/:sid", h.WithClusterAuth(h.siteSessionUpdate)) // update active session metadata (parameters)
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions/:sid/events/stream", h.WithClusterAuth(h.siteSessionStream)) // get active session's byte stream (from events)
// recorded sessions handlers
h.GET("/webapi/sites/:site/events", h.WithClusterAuth(h.siteEventsGet)) // get recorded list of sessions (from events)
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions/:sid/events", h.WithClusterAuth(h.siteSessionEventsGet)) // get recorded session's timing information (from events)
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions/:sid/stream", h.siteSessionStreamGet) // get recorded session's bytes (from events)
// OIDC related callback handlers
h.GET("/webapi/oidc/login/web", httplib.MakeHandler(h.oidcLoginWeb))
h.POST("/webapi/oidc/login/console", httplib.MakeHandler(h.oidcLoginConsole))
h.GET("/webapi/oidc/callback", httplib.MakeHandler(h.oidcCallback))
// SAML 2.0 handlers
h.POST("/webapi/saml/acs", httplib.MakeHandler(h.samlACS))
h.GET("/webapi/saml/sso", httplib.MakeHandler(h.samlSSO))
h.POST("/webapi/saml/login/console", httplib.MakeHandler(h.samlSSOConsole))
// U2F related APIs
h.GET("/webapi/u2f/signuptokens/:token", httplib.MakeHandler(h.u2fRegisterRequest))
h.POST("/webapi/u2f/users", httplib.MakeHandler(h.createNewU2FUser))
h.POST("/webapi/u2f/signrequest", httplib.MakeHandler(h.u2fSignRequest))
h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.createSessionWithU2FSignResponse))
h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.createSSHCertWithU2FSignResponse))
// trusted clusters
h.POST("/webapi/trustedclusters/validate", httplib.MakeHandler(h.validateTrustedCluster))
// User Status (used by client to check if user session is valid)
h.GET("/webapi/user/status", h.WithAuth(h.getUserStatus))
h.GET("/webapi/user/context", h.WithAuth(h.getUserContext))
// if Web UI is enabled, check the assets dir:
var (
writeSettings http.HandlerFunc
indexPage *template.Template
staticFS http.FileSystem
)
if !cfg.DisableUI {
staticFS, err = NewStaticFileSystem(isDebugMode())
if err != nil {
return nil, trace.Wrap(err)
}
index, err := staticFS.Open("/index.html")
if err != nil {
log.Error(err)
return nil, trace.Wrap(err)
}
defer index.Close()
indexContent, err := ioutil.ReadAll(index)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
indexPage, err = template.New("index").Parse(string(indexContent))
if err != nil {
return nil, trace.BadParameter("failed parsing index.html template: %v", err)
}
writeSettings = httplib.MakeStdHandler(h.getConfigurationSettings)
}
routingHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// request is going to the API?
if strings.HasPrefix(r.URL.Path, apiPrefix) {
http.StripPrefix(apiPrefix, h).ServeHTTP(w, r)
return
}
// request is going to the web UI
if cfg.DisableUI {
w.WriteHeader(http.StatusNotImplemented)
return
}
// redirect to "/web" when someone hits "/"
if r.URL.Path == "/" {
http.Redirect(w, r, "/web", http.StatusFound)
return
}
// serve Web UI:
if strings.HasPrefix(r.URL.Path, "/web/app") {
http.StripPrefix("/web", http.FileServer(staticFS)).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/web/config.js") {
writeSettings.ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/web") {
ctx, err := h.AuthenticateRequest(w, r, false)
session := struct {
Session string
}{Session: base64.StdEncoding.EncodeToString([]byte("{}"))}
if err == nil {
re, err := NewSessionResponse(ctx)
if err == nil {
out, err := json.Marshal(re)
if err == nil {
session.Session = base64.StdEncoding.EncodeToString(out)
}
}
}
httplib.SetIndexHTMLHeaders(w.Header())
indexPage.Execute(w, session)
} else {
http.NotFound(w, r)
}
})
h.NotFound = routingHandler
plugin := GetPlugin()
if plugin != nil {
plugin.AddHandlers(h)
}
return &RewritingHandler{
Handler: httplib.RewritePaths(h,
httplib.Rewrite("/webapi/sites/([^/]+)/sessions/(.*)", "/webapi/sites/$1/namespaces/default/sessions/$2"),
httplib.Rewrite("/webapi/sites/([^/]+)/sessions", "/webapi/sites/$1/namespaces/default/sessions"),
httplib.Rewrite("/webapi/sites/([^/]+)/nodes", "/webapi/sites/$1/namespaces/default/nodes"),
httplib.Rewrite("/webapi/sites/([^/]+)/connect", "/webapi/sites/$1/namespaces/default/connect"),
),
handler: h,
}, nil
}
// Close closes associated session cache operations
func (m *Handler) Close() error {
return m.auth.Close()
}
func (m *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
return ok(), nil
}
// getUserContext returns user context
//
// GET /webapi/user/context
//
func (m *Handler) getUserContext(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
clt, err := c.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
user, err := clt.GetUser(c.GetUser())
if err != nil {
return nil, trace.Wrap(err)
}
userRoleSet, err := services.FetchRoles(user.GetRoles(), clt, user.GetTraits())
if err != nil {
return nil, trace.Wrap(err)
}
userContext, err := ui.NewUserContext(user, userRoleSet)
if err != nil {
return nil, trace.Wrap(err)
}
return userContext, nil
}
func localSettings(authClient auth.ClientI, cap services.AuthPreference) (client.AuthenticationSettings, error) {
as := client.AuthenticationSettings{
Type: teleport.Local,
SecondFactor: cap.GetSecondFactor(),
}
// if the type is u2f, pull some additional data back
if cap.GetSecondFactor() == teleport.U2F {
u2fs, err := cap.GetU2F()
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
as.U2F = &client.U2FSettings{AppID: u2fs.AppID}
}
return as, nil
}
func oidcSettings(connector services.OIDCConnector) client.AuthenticationSettings {
return client.AuthenticationSettings{
Type: teleport.OIDC,
OIDC: &client.OIDCSettings{
Name: connector.GetName(),
Display: connector.GetDisplay(),
},
}
}
func samlSettings(connector services.SAMLConnector) client.AuthenticationSettings {
return client.AuthenticationSettings{
Type: teleport.SAML,
SAML: &client.SAMLSettings{
Name: connector.GetName(),
Display: connector.GetDisplay(),
},
}
}
func defaultAuthenticationSettings(authClient auth.ClientI) (client.AuthenticationSettings, error) {
cap, err := authClient.GetAuthPreference()
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
var as client.AuthenticationSettings
switch cap.GetType() {
case teleport.Local:
as, err = localSettings(authClient, cap)
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
case teleport.OIDC:
if cap.GetConnectorName() != "" {
oidcConnector, err := authClient.GetOIDCConnector(cap.GetConnectorName(), false)
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
as = oidcSettings(oidcConnector)
} else {
oidcConnectors, err := authClient.GetOIDCConnectors(false)
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
if len(oidcConnectors) == 0 {
return client.AuthenticationSettings{}, trace.BadParameter("no oidc connectors found")
}
as = oidcSettings(oidcConnectors[0])
}
case teleport.SAML:
if cap.GetConnectorName() != "" {
samlConnector, err := authClient.GetSAMLConnector(cap.GetConnectorName(), false)
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
as = samlSettings(samlConnector)
} else {
samlConnectors, err := authClient.GetSAMLConnectors(false)
if err != nil {
return client.AuthenticationSettings{}, trace.Wrap(err)
}
if len(samlConnectors) == 0 {
return client.AuthenticationSettings{}, trace.BadParameter("no saml connectors found")
}
as = samlSettings(samlConnectors[0])
}
default:
return client.AuthenticationSettings{}, trace.BadParameter("unknown type %v", cap.GetType())
}
return as, nil
}
func (m *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var err error
defaultSettings, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
if err != nil {
return nil, trace.Wrap(err)
}
return client.PingResponse{
Auth: defaultSettings,
ServerVersion: teleport.Version,
}, nil
}
func (m *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
authClient := m.cfg.ProxyClient
connectorName := p.ByName("connector")
if connectorName == teleport.Local {
cap, err := authClient.GetAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
}
as, err := localSettings(m.cfg.ProxyClient, cap)
if err != nil {
return nil, trace.Wrap(err)
}
return &client.PingResponse{
Auth: as,
ServerVersion: teleport.Version,
}, nil
}
// first look for a oidc connector with that name
oidcConnector, err := authClient.GetOIDCConnector(connectorName, false)
if err == nil {
return &client.PingResponse{
Auth: oidcSettings(oidcConnector),
ServerVersion: teleport.Version,
}, nil
}
// if no oidc connector was found, look for a saml connector
samlConnector, err := authClient.GetSAMLConnector(connectorName, false)
if err == nil {
return &client.PingResponse{
Auth: samlSettings(samlConnector),
ServerVersion: teleport.Version,
}, nil
}
return nil, trace.BadParameter("invalid connector name %v", connectorName)
}
type webConfig struct {
// Auth contains the forms of authentication the auth server supports.
Auth *client.AuthenticationSettings `json:"auth,omitempty"`
// ServerVersion is the version of Teleport that is running.
ServerVersion string `json:"serverVersion"`
}
// getConfigurationSettings returns configuration for the web application.
func (m *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Request) (interface{}, error) {
as, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
if err != nil {
log.Infof("Cannot retrieve cluster auth preferences: %v", err)
}
webCfg := webConfig{
Auth: &as,
ServerVersion: teleport.Version,
}
out, err := json.Marshal(webCfg)
if err != nil {
return nil, trace.Wrap(err)
}
fmt.Fprintf(w, "var GRV_CONFIG = %v;", string(out))
return nil, nil
}
func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Infof("oidcLoginWeb start")
query := r.URL.Query()
clientRedirectURL := query.Get("redirect_url")
if clientRedirectURL == "" {
return nil, trace.BadParameter("missing redirect_url query parameter")
}
connectorID := query.Get("connector_id")
if connectorID == "" {
return nil, trace.BadParameter("missing connector_id query parameter")
}
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
services.OIDCAuthRequest{
ConnectorID: connectorID,
CreateWebSession: true,
ClientRedirectURL: clientRedirectURL,
CheckUser: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
http.Redirect(w, r, response.RedirectURL, http.StatusFound)
return nil, nil
}
func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Debugf("oidcLoginConsole start")
var req *client.SSOLoginConsoleReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
if req.RedirectURL == "" {
return nil, trace.BadParameter("missing RedirectURL")
}
if len(req.PublicKey) == 0 {
return nil, trace.BadParameter("missing PublicKey")
}
if req.ConnectorID == "" {
return nil, trace.BadParameter("missing ConnectorID")
}
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
services.OIDCAuthRequest{
ConnectorID: req.ConnectorID,
ClientRedirectURL: req.RedirectURL,
PublicKey: req.PublicKey,
CertTTL: req.CertTTL,
CheckUser: true,
Compatibility: req.Compatibility,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil
}
func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Debugf("oidcCallback start")
response, err := m.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
if err != nil {
log.Warningf("[OIDC] Error while processing callback: %v", err)
// redirect to an error page
pathToError := url.URL{
Path: "/web/msg/error/login_failed",
RawQuery: url.Values{"details": []string{"Unable to process callback from OIDC provider."}}.Encode(),
}
http.Redirect(w, r, pathToError.String(), http.StatusFound)
return nil, nil
}
// if we created web session, set session cookie and redirect to original url
if response.Req.CreateWebSession {
log.Infof("oidcCallback redirecting to web browser")
if err := SetSession(w, response.Username, response.Session.GetName()); err != nil {
return nil, trace.Wrap(err)
}
http.Redirect(w, r, response.Req.ClientRedirectURL, http.StatusFound)
return nil, nil
}
log.Infof("oidcCallback redirecting to console login")
if len(response.Req.PublicKey) == 0 {
return nil, trace.BadParameter("not a web or console oidc login request")
}
redirectURL, err := ConstructSSHResponse(AuthParams{
ClientRedirectURL: response.Req.ClientRedirectURL,
Username: response.Username,
Identity: response.Identity,
Session: response.Session,
Cert: response.Cert,
HostSigners: response.HostSigners,
})
if err != nil {
return nil, trace.Wrap(err)
}
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
return nil, nil
}
// AuthParams are used to construct redirect URL containing auth
// information back to tsh login
type AuthParams struct {
// Username is authenticated teleport username
Username string
// Identity contains validated OIDC identity
Identity services.ExternalIdentity
// Web session will be generated by auth server if requested in OIDCAuthRequest
Session services.WebSession
// Cert will be generated by certificate authority
Cert []byte
// HostSigners is a list of signing host public keys
// trusted by proxy, used in console login
HostSigners []services.CertAuthority
// ClientRedirectURL is a URL to redirect client to
ClientRedirectURL string
}
// ConstructSSHResponse creates a special SSH response for SSH login method
// that encodes everything using the client's secret key
func ConstructSSHResponse(response AuthParams) (*url.URL, error) {
u, err := url.Parse(response.ClientRedirectURL)
if err != nil {
return nil, trace.Wrap(err)
}
signers, err := services.CertAuthoritiesToV1(response.HostSigners)
if err != nil {
return nil, trace.Wrap(err)
}
consoleResponse := client.SSHLoginResponse{
Username: response.Username,
Cert: response.Cert,
HostSigners: signers,
}
out, err := json.Marshal(consoleResponse)
if err != nil {
return nil, trace.Wrap(err)
}
values := u.Query()
secretKey := values.Get("secret")
if secretKey == "" {
return nil, trace.BadParameter("missing secret")
}
values.Set("secret", "") // remove secret so others can't see it
secretKeyBytes, err := secret.EncodedStringToKey(secretKey)
if err != nil {
return nil, trace.BadParameter("bad secret")
}
encryptor, err := secret.New(&secret.Config{KeyBytes: secretKeyBytes})
if err != nil {
return nil, trace.Wrap(err)
}
sealedBytes, err := encryptor.Seal(out)
if err != nil {
return nil, trace.Wrap(err)
}
sealedBytesData, err := json.Marshal(sealedBytes)
if err != nil {
return nil, trace.Wrap(err)
}
values.Set("response", string(sealedBytesData))
u.RawQuery = values.Encode()
return u, nil
}
// createSessionReq is a request to create session from username, password and second
// factor token
type createSessionReq struct {
User string `json:"user"`
Pass string `json:"pass"`
SecondFactorToken string `json:"second_factor_token"`
}
// CreateSessionResponse returns OAuth compabible data about
// access token: https://tools.ietf.org/html/rfc6749
type CreateSessionResponse struct {
// Type is token type (bearer)
Type string `json:"type"`
// Token value
Token string `json:"token"`
// ExpiresIn sets seconds before this token is not valid
ExpiresIn int `json:"expires_in"`
}
type createSessionResponseRaw struct {
// Type is token type (bearer)
Type string `json:"type"`
// Token value
Token string `json:"token"`
// ExpiresIn sets seconds before this token is not valid
ExpiresIn int `json:"expires_in"`
}
func (r createSessionResponseRaw) response() (*CreateSessionResponse, error) {
return &CreateSessionResponse{Type: r.Type, Token: r.Token, ExpiresIn: r.ExpiresIn}, nil
}
func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
webSession := ctx.GetWebSession()
user, err := clt.GetUser(webSession.GetUser())
if err != nil {
return nil, trace.Wrap(err)
}
var roles services.RoleSet
for _, roleName := range user.GetRoles() {
role, err := clt.GetRole(roleName)
if err != nil {
return nil, trace.Wrap(err)
}
roles = append(roles, role)
}
_, err = roles.CheckLoginDuration(0)
if err != nil {
return nil, trace.Wrap(err)
}
return &CreateSessionResponse{
Type: roundtrip.AuthBearer,
Token: webSession.GetBearerToken(),
ExpiresIn: int(webSession.GetBearerTokenExpiryTime().Sub(time.Now()) / time.Second),
}, nil
}
// createSession creates a new web session based on user, pass and 2nd factor token
//
// POST /v1/webapi/sessions
//
// {"user": "alex", "pass": "abc123", "second_factor_token": "token", "second_factor_type": "totp"}
//
// Response
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createSessionReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
// get cluster preferences to see if we should login
// with password or password+otp
authClient := m.cfg.ProxyClient
cap, err := authClient.GetAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
}
var webSession services.WebSession
switch cap.GetSecondFactor() {
case teleport.OFF:
webSession, err = m.auth.AuthWithoutOTP(req.User, req.Pass)
case teleport.OTP, teleport.HOTP, teleport.TOTP:
webSession, err = m.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken)
default:
return nil, trace.AccessDenied("unknown second factor type: %q", cap.GetSecondFactor())
}
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
if err := SetSession(w, req.User, webSession.GetName()); err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(req.User, webSession.GetName())
if err != nil {
return nil, trace.AccessDenied("need auth")
}
return NewSessionResponse(ctx)
}
// deleteSession is called to sign out user
//
// DELETE /v1/webapi/sessions/:sid
//
// Response:
//
// {"message": "ok"}
//
func (m *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
if err := ctx.Invalidate(); err != nil {
return nil, trace.Wrap(err)
}
if err := ClearSession(w); err != nil {
return nil, trace.Wrap(err)
}
return ok(), nil
}
// renewSession is called to renew the session that is about to expire
// it issues the new session and generates new session cookie.
// It's important to understand that the old session becomes effectively invalid.
//
// POST /v1/webapi/sessions/renew
//
// Response
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
//
func (m *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
newSess, err := ctx.ExtendWebSession()
if err != nil {
return nil, trace.Wrap(err)
}
// transfer ownership over connections that were opened in the
// sessionContext
newContext, err := ctx.parent.ValidateSession(newSess.GetUser(), newSess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
newContext.AddClosers(ctx.TransferClosers()...)
if err := SetSession(w, newSess.GetUser(), newSess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return NewSessionResponse(newContext)
}
type renderUserInviteResponse struct {
InviteToken string `json:"invite_token"`
User string `json:"user"`
QR []byte `json:"qr"`
}
// renderUserInvite is called to show user the new user invitation page
//
// GET /v1/webapi/users/invites/:token
//
// Response:
//
// {"invite_token": "token", "user": "alex", qr: "base64-encoded-qr-code image"}
//
//
func (m *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
token := p[0].Value
user, qrCodeBytes, err := m.auth.GetUserInviteInfo(token)
if err != nil {
return nil, trace.Wrap(err)
}
return &renderUserInviteResponse{
InviteToken: token,
User: user,
QR: qrCodeBytes,
}, nil
}
// u2fRegisterRequest is called to get a U2F challenge for registering a U2F key
//
// GET /webapi/u2f/signuptokens/:token
//
// Response:
//
// {"version":"U2F_V2","challenge":"randombase64string","appId":"https://mycorp.com:3080"}
//
func (m *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
token := p[0].Value
u2fRegisterRequest, err := m.auth.GetUserInviteU2FRegisterRequest(token)
if err != nil {
return nil, trace.Wrap(err)
}
return u2fRegisterRequest, nil
}
// u2fSignRequest is called to get a U2F challenge for authenticating
//
// POST /webapi/u2f/signrequest
//
// {"user": "alex", "pass": "abc123"}
//
// Successful response:
//
// {"version":"U2F_V2","challenge":"randombase64string","keyHandle":"longbase64string","appId":"https://mycorp.com:3080"}
//
func (m *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *client.U2fSignRequestReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
u2fSignReq, err := m.auth.GetU2FSignRequest(req.User, req.Pass)
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
return u2fSignReq, nil
}
// A request from the client to send the signature from the U2F key
type u2fSignResponseReq struct {
User string `json:"user"`
U2FSignResponse u2f.SignResponse `json:"u2f_sign_response"`
}
// createSessionWithU2FSignResponse is called to sign in with a U2F signature
//
// POST /webapi/u2f/session
//
// {"user": "alex", "u2f_sign_response": { "signatureData": "signatureinbase64", "clientData": "verylongbase64string", "challenge": "randombase64string" }}
//
// Successful response:
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
func (m *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *u2fSignResponseReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.AuthWithU2FSignResponse(req.User, &req.U2FSignResponse)
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
if err := SetSession(w, req.User, sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(req.User, sess.GetName())
if err != nil {
return nil, trace.AccessDenied("need auth")
}
return NewSessionResponse(ctx)
}
// createNewUser req is a request to create a new Teleport user
type createNewUserReq struct {
InviteToken string `json:"invite_token"`
Pass string `json:"pass"`
SecondFactorToken string `json:"second_factor_token,omitempty"`
}
// createNewUser creates new user entry based on the invite token
//
// POST /v1/webapi/users
//
// {"invite_token": "unique invite token", "pass": "user password", "second_factor_token": "valid second factor token"}
//
// Sucessful response: (session cookie is set)
//
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
func (m *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createNewUserReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken)
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return NewSessionResponse(ctx)
}
// A request to create a new user which uses U2F as the second factor
type createNewU2FUserReq struct {
InviteToken string `json:"invite_token"`
Pass string `json:"pass"`
U2FRegisterResponse u2f.RegisterResponse `json:"u2f_register_response"`
}
// createNewU2FUser creates a new user configured to use U2F as the second factor
//
// POST /webapi/u2f/users
//
// {"invite_token": "unique invite token", "pass": "user password", "u2f_register_response": {"registrationData":"verylongbase64string","clientData":"longbase64string"}}
//
// Sucessful response: (session cookie is set)
//
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
func (m *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createNewU2FUserReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse)
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return NewSessionResponse(ctx)
}
type getSitesResponse struct {
Sites []site `json:"sites"`
}
type site struct {
Name string `json:"name"`
LastConnected time.Time `json:"last_connected"`
Status string `json:"status"`
}
func convertSites(rs []reversetunnel.RemoteSite) []site {
out := make([]site, len(rs))
for i := range rs {
out[i] = site{
Name: rs[i].GetName(),
LastConnected: rs[i].GetLastConnected(),
Status: rs[i].GetStatus(),
}
}
return out
}
// getSites returns a list of sites
//
// GET /v1/webapi/sites
//
// Sucessful response:
//
// {"sites": {"name": "localhost", "last_connected": "RFC3339 time", "status": "active"}}
//
func (m *Handler) getSites(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
return getSitesResponse{
Sites: convertSites(m.cfg.Proxy.GetSites()),
}, nil
}
type getSiteNamespacesResponse struct {
Namespaces []services.Namespace `json:"namespaces"`
}
/* getSiteNamespaces returns a list of namespaces for a given site
GET /v1/webapi/namespaces/:namespace/sites/:site/nodes
Sucessful response:
{"namespaces": [{..namespace resource...}]}
*/
func (m *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
log.Debugf("[web] GET /namespaces")
clt, err := site.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
namespaces, err := clt.GetNamespaces()
if err != nil {
return nil, trace.Wrap(err)
}
return getSiteNamespacesResponse{
Namespaces: namespaces,
}, nil
}
type nodeWithSessions struct {
Node services.ServerV1 `json:"node"`
Sessions []session.Session `json:"sessions"`
}
func (m *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
log.Debugf("[web] GET /nodes")
clt, err := site.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
servers, err := clt.GetNodes(namespace)
if err != nil {
return nil, trace.Wrap(err)
}
items := []interface{}{}
for _, server := range servers {
items = append(items, server.V1())
}
return makeResponse(items)
}
// siteNodeConnect connect to the site node
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/connect?access_token=bearer_token&params=<urlencoded json-structure>
//
// Due to the nature of websocket we can't POST parameters as is, so we have
// to add query parameters. The params query parameter is a url encodeed JSON strucrture:
//
// {"server_id": "uuid", "login": "admin", "term": {"h": 120, "w": 100}, "sid": "123"}
//
// Session id can be empty
//
// Sucessful response is a websocket stream that allows read write to the server
//
func (m *Handler) siteNodeConnect(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
ctx *SessionContext,
site reversetunnel.RemoteSite) (interface{}, error) {
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
q := r.URL.Query()
params := q.Get("params")
if params == "" {
return nil, trace.BadParameter("missing params")
}
var req *terminalRequest
if err := json.Unmarshal([]byte(params), &req); err != nil {
return nil, trace.Wrap(err)
}
log.Debugf("[WEB] new terminal request for ns=%s, server=%s, login=%s, sid=%s",
req.Namespace, req.Server, req.Login, req.SessionID)
req.Namespace = namespace
req.ProxyHostPort = m.ProxyHostPort()
req.Cluster = site.GetName()
clt, err := ctx.GetUserClient(site)
if err != nil {
return nil, trace.Wrap(err)
}
term, err := newTerminal(*req, clt, ctx)
if err != nil {
log.Errorf("[WEB] Unable to create terminal: %v", err)
return nil, trace.Wrap(err)
}
// start the websocket session with a web-based terminal:
log.Infof("[WEB] getting terminal to '%#v'", req)
term.Run(w, r)
return nil, nil
}
// sessionStreamEvent is sent over the session stream socket, it contains
// last events that occured (only new events are sent)
type sessionStreamEvent struct {
Events []events.EventFields `json:"events"`
Session *session.Session `json:"session"`
Servers []services.ServerV1 `json:"servers"`
}
// siteSessionStream returns a stream of events related to the session
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions/:sid/events/stream?access_token=bearer_token
//
// Sucessful response is a websocket stream that allows read write to the server and returns
// json events
//
func (m *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
connect, err := newSessionStreamHandler(namespace,
*sessionID, ctx, site, m.sessionStreamPollPeriod)
if err != nil {
return nil, trace.Wrap(err)
}
// this is to make sure we close web socket connections once
// sessionContext that owns them expires
ctx.AddClosers(connect)
defer func() {
connect.Close()
ctx.RemoveCloser(connect)
}()
connect.Handler().ServeHTTP(w, r)
return nil, 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
//
// POST /v1/webapi/sites/:site/sessions
//
// Request body:
//
// {"session": {"terminal_params": {"w": 100, "h": 100}, "login": "centos"}}
//
// Response body:
//
// {"session": {"id": "session-id", "terminal_params": {"w": 100, "h": 100}, "login": "centos"}}
//
func (m *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
var req *siteSessionGenerateReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
req.Session.ID = session.NewID()
req.Session.Created = time.Now().UTC()
req.Session.LastActive = time.Now().UTC()
req.Session.Namespace = namespace
log.Infof("Generated session: %#v", req.Session)
return siteSessionGenerateResponse{Session: req.Session}, nil
}
type siteSessionUpdateReq struct {
TerminalParams session.TerminalParams `json:"terminal_params"`
}
// siteSessionUpdate udpdates the site session
//
// PUT /v1/webapi/sites/:site/sessions/:sid
//
// Request body:
//
// {"terminal_params": {"w": 100, "h": 100}}
//
// Response body:
//
// {"message": "ok"}
//
func (m *Handler) siteSessionUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
}
var req *siteSessionUpdateReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
siteAPI, err := site.GetClient()
if err != nil {
log.Error(err)
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
err = ctx.UpdateSessionTerminal(siteAPI, namespace, *sessionID, req.TerminalParams)
if err != nil {
log.Error(err)
return nil, trace.Wrap(err)
}
return ok(), nil
}
type siteSessionsGetResponse struct {
Sessions []session.Session `json:"sessions"`
}
// siteSessionGet gets the list of site sessions filtered by creation time
// and either they're active or not
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions
//
// Response body:
//
// {"sessions": [{"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}, ...] }
func (m *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
clt, err := ctx.GetUserClient(site)
if err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
sessions, err := clt.GetSessions(namespace)
if err != nil {
return nil, trace.Wrap(err)
}
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 (m *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("web.getSetssion(%v)", sessionID)
clt, err := ctx.GetUserClient(site)
if err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
sess, err := clt.GetSession(namespace, *sessionID)
if err != nil {
return nil, trace.Wrap(err)
}
return *sess, nil
}
const maxStreamBytes = 5 * 1024 * 1024
// siteEventsGet allows to search for events on site
//
// GET /v1/webapi/sites/:site/events
//
// Query parameters:
// "from" : date range from, encoded as RFC3339
// "to" : date range to, encoded as RFC3339
// ... : the rest of the query string is passed to the search back-end as-is,
// the default backend performs exact search: ?key=value means "event
// with a field 'key' with value 'value'
//
func (m *Handler) siteEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
query := r.URL.Query()
log.Infof("web.getEvents(%v)", r.URL.RawQuery)
clt, err := ctx.GetUserClient(site)
if err != nil {
log.Error(err)
return nil, trace.Wrap(err)
}
// default values
to := time.Now().In(time.UTC)
from := to.AddDate(0, -1, 0) // one month ago
// parse 'to' and 'from' params:
fromStr := query.Get("from")
if fromStr != "" {
from, err = time.Parse(time.RFC3339, fromStr)
if err != nil {
return nil, trace.BadParameter("from")
}
}
toStr := query.Get("to")
if toStr != "" {
to, err = time.Parse(time.RFC3339, toStr)
if err != nil {
return nil, trace.BadParameter("to")
}
}
el, err := clt.SearchSessionEvents(from, to)
if err != nil {
return nil, trace.Wrap(err)
}
return eventsListGetResponse{Events: el}, nil
}
type siteSessionStreamGetResponse struct {
Bytes []byte `json:"bytes"`
}
// siteSessionStreamGet returns a byte array from a session's stream
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions/:sid/stream?query
//
// Query parameters:
// "offset" : bytes from the beginning
// "bytes" : number of bytes to read (it won't return more than 512Kb)
//
// Unlike other request handlers, this one does not return JSON.
// It returns the binary stream unencoded, directly in the respose body,
// with Content-Type of application/octet-stream, gzipped with up to 95%
// compression ratio.
func (m *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var site reversetunnel.RemoteSite
onError := func(err error) {
w.Header().Set("Content-Type", "text/json")
trace.WriteError(w, err)
}
// authenticate first:
ctx, err := m.AuthenticateRequest(w, r, true)
if err != nil {
log.Info(err)
// clear session just in case if the authentication request is not valid
ClearSession(w)
onError(err)
return
}
// get the site interface:
siteName := p.ByName("site")
if siteName == currentSiteShortcut {
sites := m.cfg.Proxy.GetSites()
if len(sites) < 1 {
onError(trace.NotFound("no active sites"))
return
}
siteName = sites[0].GetName()
}
site, err = m.cfg.Proxy.GetSite(siteName)
if err != nil {
onError(err)
return
}
// get the session:
sid, err := session.ParseID(p.ByName("sid"))
if err != nil {
onError(trace.Wrap(err))
return
}
clt, err := ctx.GetUserClient(site)
if err != nil {
onError(trace.Wrap(err))
return
}
// look at 'offset' parameter
query := r.URL.Query()
offset, _ := strconv.Atoi(query.Get("offset"))
if err != nil {
onError(trace.Wrap(err))
return
}
max, err := strconv.Atoi(query.Get("bytes"))
if err != nil || max <= 0 {
max = maxStreamBytes
}
if max > maxStreamBytes {
max = maxStreamBytes
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
onError(trace.BadParameter("invalid namespace %q", namespace))
return
}
// call the site API to get the chunk:
bytes, err := clt.GetSessionChunk(namespace, *sid, offset, max)
if err != nil {
onError(trace.Wrap(err))
return
}
// see if we can gzip it:
var writer io.Writer = w
for _, acceptedEnc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
if strings.TrimSpace(acceptedEnc) == "gzip" {
gzipper := gzip.NewWriter(w)
writer = gzipper
defer gzipper.Close()
w.Header().Set("Content-Encoding", "gzip")
}
}
w.Header().Set("Content-Type", "application/octet-stream")
_, err = writer.Write(bytes)
if err != nil {
onError(trace.Wrap(err))
return
}
}
type eventsListGetResponse struct {
Events []events.EventFields `json:"events"`
}
// siteSessionEventsGet gets the site session by id
//
// GET /v1/webapi/sites/:site/namespaces/:namespace/sessions/:sid/events?after=N
//
// Query:
// "after" : cursor value of an event to return "newer than" events
// good for repeated polling
//
// Response body (each event is an arbitrary JSON structure)
//
// {"events": [{...}, {...}, ...}
//
func (m *Handler) siteSessionEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := ctx.GetUserClient(site)
if err != nil {
return nil, trace.Wrap(err)
}
afterN, err := strconv.Atoi(r.URL.Query().Get("after"))
if err != nil {
afterN = 0
}
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
e, err := clt.GetSessionEvents(namespace, *sessionID, afterN)
if err != nil {
return nil, trace.Wrap(err)
}
return eventsListGetResponse{Events: e}, nil
}
// createSSHCert is a web call that generates new SSH certificate based
// on user's name, password, 2nd factor token and public key user wishes to sign
//
// POST /v1/webapi/ssh/certs
//
// { "user": "bob", "password": "pass", "otp_token": "tok", "pub_key": "key to sign", "ttl": 1000000000 }
//
// Success response
//
// { "cert": "base64 encoded signed cert", "host_signers": [{"domain_name": "example.com", "checking_keys": ["base64 encoded public signing key"]}] }
//
func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *client.CreateSSHCertReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
authClient := h.cfg.ProxyClient
cap, err := authClient.GetAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
}
var cert *client.SSHLoginResponse
switch cap.GetSecondFactor() {
case teleport.OFF:
cert, err = h.auth.GetCertificateWithoutOTP(*req)
case teleport.OTP, teleport.HOTP, teleport.TOTP:
// convert legacy requests to new parameter here. remove once migration to TOTP is complete.
if req.HOTPToken != "" {
req.OTPToken = req.HOTPToken
}
cert, err = h.auth.GetCertificateWithOTP(*req)
default:
return nil, trace.AccessDenied("unknown second factor type: %q", cap.GetSecondFactor())
}
if err != nil {
return nil, trace.Wrap(err)
}
return cert, nil
}
// createSSHCertWithU2FSignResponse is a web call that generates new SSH certificate based
// on user's name, password, U2F signature and public key user wishes to sign
//
// POST /v1/webapi/u2f/certs
//
// { "user": "bob", "password": "pass", "u2f_sign_response": { "signatureData": "signatureinbase64", "clientData": "verylongbase64string", "challenge": "randombase64string" }, "pub_key": "key to sign", "ttl": 1000000000 }
//
// Success response
//
// { "cert": "base64 encoded signed cert", "host_signers": [{"domain_name": "example.com", "checking_keys": ["base64 encoded public signing key"]}] }
//
func (h *Handler) createSSHCertWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *client.CreateSSHCertWithU2FReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
cert, err := h.auth.GetCertificateWithU2F(*req)
if err != nil {
return nil, trace.Wrap(err)
}
return cert, nil
}
// validateTrustedCluster validates the token for a trusted cluster and returns it's own host and user certificate authority.
//
// POST /webapi/trustedclusters/validate
//
// * Request body:
//
// {
// "token": "foo",
// "certificate_authorities": ["AQ==", "Ag=="]
// }
//
// * Response:
//
// {
// "certificate_authorities": ["AQ==", "Ag=="]
// }
func (h *Handler) validateTrustedCluster(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var validateRequestRaw auth.ValidateTrustedClusterRequestRaw
if err := httplib.ReadJSON(r, &validateRequestRaw); err != nil {
return nil, trace.Wrap(err)
}
validateRequest, err := validateRequestRaw.ToNative()
if err != nil {
return nil, trace.Wrap(err)
}
validateResponse, err := h.auth.ValidateTrustedCluster(validateRequest)
if err != nil {
return nil, trace.AccessDenied("invalid token")
}
validateResponseRaw, err := validateResponse.ToRaw()
if err != nil {
return nil, trace.Wrap(err)
}
return validateResponseRaw, nil
}
func (h *Handler) String() string {
return fmt.Sprintf("multi site")
}
// currentSiteShortcut is a special shortcut that will return the first
// available site, is helpful when UI works in single site mode to reduce
// the amount of requests
const currentSiteShortcut = "-current-"
// ContextHandler is a handler called with the auth context, what means it is authenticated and ready to work
type ContextHandler func(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext) (interface{}, error)
// ClusterHandler is a authenticated handler that is called for some existing remote cluster
type ClusterHandler func(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error)
// WithClusterAuth ensures that request is authenticated and is issued for existing cluster
func (h *Handler) WithClusterAuth(fn ClusterHandler) httprouter.Handle {
return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
ctx, err := h.AuthenticateRequest(w, r, true)
if err != nil {
log.Info(err)
// clear session just in case if the authentication request is not valid
ClearSession(w)
return nil, trace.Wrap(err)
}
siteName := p.ByName("site")
if siteName == currentSiteShortcut {
sites := h.cfg.Proxy.GetSites()
if len(sites) < 1 {
return nil, trace.NotFound("no active sites")
}
siteName = sites[0].GetName()
}
site, err := h.cfg.Proxy.GetSite(siteName)
if err != nil {
log.Warn(err)
return nil, trace.Wrap(err)
}
return fn(w, r, p, ctx, site)
})
}
// WithAuth ensures that request is authenticated
func (h *Handler) WithAuth(fn ContextHandler) httprouter.Handle {
return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
ctx, err := h.AuthenticateRequest(w, r, true)
if err != nil {
return nil, trace.Wrap(err)
}
return fn(w, r, p, ctx)
})
}
// authenticateRequest authenticates request using combination of a session cookie
// and bearer token
func (h *Handler) AuthenticateRequest(w http.ResponseWriter, r *http.Request, checkBearerToken bool) (*SessionContext, error) {
const missingCookieMsg = "missing session cookie"
logger := log.WithFields(log.Fields{
"request": fmt.Sprintf("%v %v", r.Method, r.URL.Path),
})
cookie, err := r.Cookie("session")
if err != nil || (cookie != nil && cookie.Value == "") {
if err != nil {
logger.Warn(err)
}
return nil, trace.AccessDenied(missingCookieMsg)
}
d, err := DecodeCookie(cookie.Value)
if err != nil {
logger.Warningf("failed to decode cookie: %v", err)
return nil, trace.AccessDenied("failed to decode cookie")
}
ctx, err := h.auth.ValidateSession(d.User, d.SID)
if err != nil {
logger.Warningf("invalid session: %v", err)
ClearSession(w)
return nil, trace.AccessDenied("need auth")
}
if checkBearerToken {
creds, err := roundtrip.ParseAuthHeaders(r)
if err != nil {
logger.Warningf("no auth headers %v", err)
return nil, trace.AccessDenied("need auth")
}
if creds.Password != ctx.GetWebSession().GetBearerToken() {
logger.Warningf("bad bearer token")
return nil, trace.AccessDenied("bad bearer token")
}
}
return ctx, nil
}
// ProxyHostPort returns the address of the proxy server using --proxy
// notation, i.e. "localhost:8030,8023"
func (h *Handler) ProxyHostPort() string {
// addr equals to "localhost:8030" at this point
addr := h.cfg.ProxyWebAddr.String()
// add the SSH port number and return
_, sshPort, err := net.SplitHostPort(h.cfg.ProxySSHAddr.String())
if err != nil {
log.Error(err)
sshPort = strconv.Itoa(defaults.SSHProxyListenPort)
}
return fmt.Sprintf("%s,%s", addr, sshPort)
}
func message(msg string) interface{} {
return map[string]interface{}{"message": msg}
}
func ok() interface{} {
return message("ok")
}
type Server struct {
http.Server
}
// CreateSignupLink generates and returns a URL which is given to a new
// user to complete registration with Teleport via Web UI
func CreateSignupLink(hostPort string, token string) string {
return "https://" + hostPort + "/web/newuser/" + token
}
type responseData struct {
Items interface{} `json:"items"`
}
func makeResponse(items interface{}) (interface{}, error) {
return responseData{Items: items}, nil
}