diff --git a/integration/app_integration_test.go b/integration/app_integration_test.go index 5d5f1e69843bc..49014635735b8 100644 --- a/integration/app_integration_test.go +++ b/integration/app_integration_test.go @@ -765,7 +765,7 @@ func (p *pack) createAppSession(t *testing.T, publicAddr, clusterName string) st require.NotEmpty(t, p.webToken) casReq, err := json.Marshal(web.CreateAppSessionRequest{ - FQDN: publicAddr, + FQDNHint: publicAddr, PublicAddr: publicAddr, ClusterName: clusterName, }) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index ee831bff20fcb..02ead42206e3b 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -351,6 +351,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { h.POST("/webapi/trustedcluster", h.WithAuth(h.upsertTrustedClusterHandle)) h.DELETE("/webapi/trustedcluster/:name", h.WithAuth(h.deleteTrustedCluster)) + h.GET("/webapi/apps/:fqdnHint", h.WithAuth(h.getAppFQDN)) + h.GET("/webapi/apps/:fqdnHint/:clusterName/:publicAddr", h.WithAuth(h.getAppFQDN)) + // if Web UI is enabled, check the assets dir: var indexPage *template.Template if cfg.StaticFS != nil { @@ -1246,7 +1249,7 @@ func newSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) { // createWebSession creates a new web session based on user, pass and 2nd factor token // -// POST /v1/webapi/sessions +// POST /v1/webapi/sessions/web // // {"user": "alex", "pass": "abc123", "second_factor_token": "token", "second_factor_type": "totp"} // diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index e0b27371434bd..cf894debe12fa 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -1837,7 +1837,7 @@ func TestApplicationAccessDisabled(t *testing.T) { endpoint := pack.clt.Endpoint("webapi", "sessions", "app") _, err = pack.clt.PostJSON(context.Background(), endpoint, &CreateAppSessionRequest{ - FQDN: "panel.example.com", + FQDNHint: "panel.example.com", PublicAddr: "panel.example.com", ClusterName: "localhost", }) @@ -1890,7 +1890,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Valid request: all fields."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com", + FQDNHint: "panel.example.com", PublicAddr: "panel.example.com", ClusterName: "localhost", }, @@ -1911,7 +1911,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Valid request: only FQDN."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com", + FQDNHint: "panel.example.com", }, outError: false, outFQDN: "panel.example.com", @@ -1934,7 +1934,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Invalid application."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com", + FQDNHint: "panel.example.com", PublicAddr: "invalid.example.com", ClusterName: "localhost", }, @@ -1943,7 +1943,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Invalid cluster name."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com", + FQDNHint: "panel.example.com", PublicAddr: "panel.example.com", ClusterName: "example.com", }, @@ -1952,7 +1952,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Malicious request: all fields."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com@malicious.com", + FQDNHint: "panel.example.com@malicious.com", PublicAddr: "panel.example.com", ClusterName: "localhost", }, @@ -1963,7 +1963,7 @@ func (s *WebSuite) TestCreateAppSession(c *C) { { inComment: Commentf("Malicious request: only FQDN."), inCreateRequest: &CreateAppSessionRequest{ - FQDN: "panel.example.com@malicious.com", + FQDNHint: "panel.example.com@malicious.com", }, outError: true, }, diff --git a/lib/web/app/fragment.go b/lib/web/app/fragment.go index a4d0ad1e1a217..dd2402cccf429 100644 --- a/lib/web/app/fragment.go +++ b/lib/web/app/fragment.go @@ -43,21 +43,22 @@ type fragmentRequest struct { func (h *Handler) handleFragment(w http.ResponseWriter, r *http.Request, p httprouter.Params) error { switch r.Method { case http.MethodGet: + q := r.URL.Query() // If the state query parameter is not set, generate a new state token, // store it in a cookie and redirect back to the app launcher. - if r.URL.Query().Get("state") == "" { + if q.Get("state") == "" { stateToken, err := utils.CryptoRandomHex(auth.TokenLenBytes) if err != nil { h.log.WithError(err).Debugf("Failed to generate and encode random numbers.") return trace.AccessDenied("access denied") } h.setAuthStateCookie(w, stateToken) - p := launcherURLParams{ - clusterName: r.URL.Query().Get("cluster"), - publicAddr: r.URL.Query().Get("addr"), + urlParams := launcherURLParams{ + clusterName: q.Get("cluster"), + publicAddr: q.Get("addr"), stateToken: stateToken, } - return h.redirectToLauncher(w, r, p) + return h.redirectToLauncher(w, r, urlParams) } nonce, err := utils.CryptoRandomHex(auth.TokenLenBytes) diff --git a/lib/web/apps.go b/lib/web/apps.go index bce432e24551e..325f40717d4ea 100644 --- a/lib/web/apps.go +++ b/lib/web/apps.go @@ -55,17 +55,15 @@ func (h *Handler) siteAppsGet(w http.ResponseWriter, r *http.Request, p httprout return makeResponse(ui.MakeApps(h.auth.clusterName, h.proxyDNSName(), appClusterName, appServers)) } -type CreateAppSessionRequest struct { - // FQDN is the fully qualified domain name of the application. - FQDN string `json:"fqdn"` - - // PublicAddr is the public address of the application. - PublicAddr string `json:"public_addr"` +type GetAppFQDNRequest resolveAppParams - // ClusterName is the cluster within which this application is running. - ClusterName string `json:"cluster_name"` +type GetAppFQDNResponse struct { + // FQDN is application FQDN. + FQDN string `json:"fqdn"` } +type CreateAppSessionRequest resolveAppParams + type CreateAppSessionResponse struct { // CookieValue is the application session cookie value. CookieValue string `json:"value"` @@ -73,8 +71,46 @@ type CreateAppSessionResponse struct { FQDN string `json:"fqdn"` } +// getAppFQDN resolves the input params to a known application and returns +// its valid FQDN. +// +// GET /v1/webapi/apps/:fqdnHint/:clusterName/:publicAddr +func (h *Handler) getAppFQDN(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext) (interface{}, error) { + req := GetAppFQDNRequest{ + FQDNHint: p.ByName("fqdnHint"), + ClusterName: p.ByName("clusterName"), + PublicAddr: p.ByName("publicAddr"), + } + + // Get an auth client connected with the user's identity. + authClient, err := ctx.GetClient() + if err != nil { + return nil, trace.Wrap(err) + } + + // Get a reverse tunnel proxy aware of the user's permissions. + proxy, err := h.ProxyWithRoles(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + // Use the information the caller provided to attempt to resolve to an + // application running within either the root or leaf cluster. + result, err := h.resolveApp(r.Context(), authClient, proxy, resolveAppParams(req)) + if err != nil { + return nil, trace.Wrap(err, "unable to resolve FQDN: %v", req.FQDNHint) + } + + return &GetAppFQDNResponse{ + FQDN: result.FQDN, + }, nil +} + +// createAppSession creates a new application session. +// +// POST /v1/webapi/sessions/app func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext) (interface{}, error) { - var req *CreateAppSessionRequest + var req CreateAppSessionRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } @@ -93,9 +129,9 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt // Use the information the caller provided to attempt to resolve to an // application running within either the root or leaf cluster. - result, err := h.validateAppSessionRequest(r.Context(), authClient, proxy, req) + result, err := h.resolveApp(r.Context(), authClient, proxy, resolveAppParams(req)) if err != nil { - return nil, trace.Wrap(err, "Unable to resolve FQDN: %v", req.FQDN) + return nil, trace.Wrap(err, "unable to resolve FQDN: %v", req.FQDNHint) } h.log.Debugf("Creating application web session for %v in %v.", result.PublicAddr, result.ClusterName) @@ -175,7 +211,30 @@ func (h *Handler) waitForAppSession(ctx context.Context, sessionID, user string) return auth.WaitForAppSession(ctx, sessionID, user, h.cfg.AccessPoint) } -func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter, proxy reversetunnel.Tunnel, req *CreateAppSessionRequest) (*validateAppSessionResult, error) { +type resolveAppParams struct { + // FQDNHint indicates (tentatively) the fully qualified domain name of the application. + FQDNHint string `json:"fqdn"` + + // PublicAddr is the public address of the application. + PublicAddr string `json:"public_addr"` + + // ClusterName is the cluster within which this application is running. + ClusterName string `json:"cluster_name"` +} + +type resolveAppResult struct { + // ServerID is the ID of the server this application is running on. + ServerID string + // FQDN is the best effort FQDN resolved for this application. + FQDN string + // PublicAddr of application requested. + PublicAddr string + // ClusterName is the name of the cluster within which the application + // is running. + ClusterName string +} + +func (h *Handler) resolveApp(ctx context.Context, clt app.Getter, proxy reversetunnel.Tunnel, params resolveAppParams) (*resolveAppResult, error) { var ( app *services.App server services.Server @@ -186,10 +245,13 @@ func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter, // If the request contains a public address and cluster name (for example, if it came // from the application launcher in the Web UI) then directly exactly resolve the // application that the caller is requesting. If it does not, do best effort FQDN resolution. - if req.PublicAddr != "" && req.ClusterName != "" { - app, server, appClusterName, err = h.resolveDirect(ctx, proxy, req.PublicAddr, req.ClusterName) - } else { - app, server, appClusterName, err = h.resolveFQDN(ctx, clt, proxy, req.FQDN) + switch { + case params.PublicAddr != "" && params.ClusterName != "": + app, server, appClusterName, err = h.resolveDirect(ctx, proxy, params.PublicAddr, params.ClusterName) + case params.FQDNHint != "": + app, server, appClusterName, err = h.resolveFQDN(ctx, clt, proxy, params.FQDNHint) + default: + err = trace.BadParameter("no inputs to resolve application") } if err != nil { return nil, trace.Wrap(err) @@ -197,7 +259,7 @@ func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter, fqdn := ui.AssembleAppFQDN(h.auth.clusterName, h.proxyDNSName(), appClusterName, app) - return &validateAppSessionResult{ + return &resolveAppResult{ ServerID: server.GetName(), FQDN: fqdn, PublicAddr: app.PublicAddr, @@ -205,18 +267,6 @@ func (h *Handler) validateAppSessionRequest(ctx context.Context, clt app.Getter, }, nil } -type validateAppSessionResult struct { - // ServerID is the ID of the server this application is running on. - ServerID string - // FQDN is the best effort FQDN resolved for this application. - FQDN string - // PublicAddr of application requested. - PublicAddr string - // ClusterName is the name of the cluster within which the application - // is running. - ClusterName string -} - // resolveDirect takes a public address and cluster name and exactly resolves // the application and the server on which it is running. func (h *Handler) resolveDirect(ctx context.Context, proxy reversetunnel.Tunnel, publicAddr string, clusterName string) (*services.App, services.Server, string, error) {